1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.wm.shell.bubbles; 17 18 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 19 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 20 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; 21 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 22 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 23 24 import android.annotation.NonNull; 25 import android.app.PendingIntent; 26 import android.content.Context; 27 import android.content.LocusId; 28 import android.content.pm.ShortcutInfo; 29 import android.text.TextUtils; 30 import android.util.ArrayMap; 31 import android.util.ArraySet; 32 import android.util.Log; 33 import android.util.Pair; 34 import android.view.View; 35 36 import androidx.annotation.Nullable; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.util.FrameworkStatsLog; 40 import com.android.wm.shell.R; 41 import com.android.wm.shell.bubbles.Bubbles.DismissReason; 42 43 import java.io.FileDescriptor; 44 import java.io.PrintWriter; 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.Comparator; 48 import java.util.HashMap; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Objects; 52 import java.util.Set; 53 import java.util.concurrent.Executor; 54 import java.util.function.Consumer; 55 import java.util.function.Predicate; 56 57 /** 58 * Keeps track of active bubbles. 59 */ 60 public class BubbleData { 61 62 private BubbleLogger mLogger; 63 64 private int mCurrentUserId; 65 66 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; 67 68 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = 69 Comparator.comparing(BubbleData::sortKey).reversed(); 70 71 /** Contains information about changes that have been made to the state of bubbles. */ 72 static final class Update { 73 boolean expandedChanged; 74 boolean selectionChanged; 75 boolean orderChanged; 76 boolean suppressedSummaryChanged; 77 boolean expanded; 78 @Nullable BubbleViewProvider selectedBubble; 79 @Nullable Bubble addedBubble; 80 @Nullable Bubble updatedBubble; 81 @Nullable Bubble addedOverflowBubble; 82 @Nullable Bubble removedOverflowBubble; 83 @Nullable Bubble suppressedBubble; 84 @Nullable Bubble unsuppressedBubble; 85 @Nullable String suppressedSummaryGroup; 86 // Pair with Bubble and @DismissReason Integer 87 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); 88 89 // A read-only view of the bubbles list, changes there will be reflected here. 90 final List<Bubble> bubbles; 91 final List<Bubble> overflowBubbles; 92 Update(List<Bubble> row, List<Bubble> overflow)93 private Update(List<Bubble> row, List<Bubble> overflow) { 94 bubbles = Collections.unmodifiableList(row); 95 overflowBubbles = Collections.unmodifiableList(overflow); 96 } 97 anythingChanged()98 boolean anythingChanged() { 99 return expandedChanged 100 || selectionChanged 101 || addedBubble != null 102 || updatedBubble != null 103 || !removedBubbles.isEmpty() 104 || addedOverflowBubble != null 105 || removedOverflowBubble != null 106 || orderChanged 107 || suppressedBubble != null 108 || unsuppressedBubble != null 109 || suppressedSummaryChanged 110 || suppressedSummaryGroup != null; 111 } 112 bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason)113 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { 114 removedBubbles.add(new Pair<>(bubbleToRemove, reason)); 115 } 116 } 117 118 /** 119 * This interface reports changes to the state and appearance of bubbles which should be applied 120 * as necessary to the UI. 121 */ 122 interface Listener { 123 /** Reports changes have have occurred as a result of the most recent operation. */ applyUpdate(Update update)124 void applyUpdate(Update update); 125 } 126 127 interface TimeSource { currentTimeMillis()128 long currentTimeMillis(); 129 } 130 131 private final Context mContext; 132 private final BubblePositioner mPositioner; 133 private final Executor mMainExecutor; 134 /** Bubbles that are actively in the stack. */ 135 private final List<Bubble> mBubbles; 136 /** Bubbles that aged out to overflow. */ 137 private final List<Bubble> mOverflowBubbles; 138 /** Bubbles that are being loaded but haven't been added to the stack just yet. */ 139 private final HashMap<String, Bubble> mPendingBubbles; 140 /** Bubbles that are suppressed due to locusId. */ 141 private final ArrayMap<LocusId, Bubble> mSuppressedBubbles = new ArrayMap<>(); 142 /** Visible locusIds. */ 143 private final ArraySet<LocusId> mVisibleLocusIds = new ArraySet<>(); 144 145 private BubbleViewProvider mSelectedBubble; 146 private final BubbleOverflow mOverflow; 147 private boolean mShowingOverflow; 148 private boolean mExpanded; 149 private int mMaxBubbles; 150 private int mMaxOverflowBubbles; 151 152 private boolean mNeedsTrimming; 153 154 // State tracked during an operation -- keeps track of what listener events to dispatch. 155 private Update mStateChange; 156 157 private TimeSource mTimeSource = System::currentTimeMillis; 158 159 @Nullable 160 private Listener mListener; 161 162 @Nullable 163 private Bubbles.SuppressionChangedListener mSuppressionListener; 164 private Bubbles.PendingIntentCanceledListener mCancelledListener; 165 166 /** 167 * We track groups with summaries that aren't visibly displayed but still kept around because 168 * the bubble(s) associated with the summary still exist. 169 * 170 * The summary must be kept around so that developers can cancel it (and hence the bubbles 171 * associated with it). This list is used to check if the summary should be hidden from the 172 * shade. 173 * 174 * Key: group key of the notification 175 * Value: key of the notification 176 */ 177 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); 178 BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, Executor mainExecutor)179 public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, 180 Executor mainExecutor) { 181 mContext = context; 182 mLogger = bubbleLogger; 183 mPositioner = positioner; 184 mMainExecutor = mainExecutor; 185 mOverflow = new BubbleOverflow(context, positioner); 186 mBubbles = new ArrayList<>(); 187 mOverflowBubbles = new ArrayList<>(); 188 mPendingBubbles = new HashMap<>(); 189 mStateChange = new Update(mBubbles, mOverflowBubbles); 190 mMaxBubbles = mPositioner.getMaxBubbles(); 191 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); 192 } 193 setSuppressionChangedListener( Bubbles.SuppressionChangedListener listener)194 public void setSuppressionChangedListener( 195 Bubbles.SuppressionChangedListener listener) { 196 mSuppressionListener = listener; 197 } 198 setPendingIntentCancelledListener( Bubbles.PendingIntentCanceledListener listener)199 public void setPendingIntentCancelledListener( 200 Bubbles.PendingIntentCanceledListener listener) { 201 mCancelledListener = listener; 202 } 203 onMaxBubblesChanged()204 public void onMaxBubblesChanged() { 205 mMaxBubbles = mPositioner.getMaxBubbles(); 206 if (!mExpanded) { 207 trim(); 208 dispatchPendingChanges(); 209 } else { 210 mNeedsTrimming = true; 211 } 212 } 213 hasBubbles()214 public boolean hasBubbles() { 215 return !mBubbles.isEmpty(); 216 } 217 hasOverflowBubbles()218 public boolean hasOverflowBubbles() { 219 return !mOverflowBubbles.isEmpty(); 220 } 221 isExpanded()222 public boolean isExpanded() { 223 return mExpanded; 224 } 225 hasAnyBubbleWithKey(String key)226 public boolean hasAnyBubbleWithKey(String key) { 227 return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key); 228 } 229 hasBubbleInStackWithKey(String key)230 public boolean hasBubbleInStackWithKey(String key) { 231 return getBubbleInStackWithKey(key) != null; 232 } 233 hasOverflowBubbleWithKey(String key)234 public boolean hasOverflowBubbleWithKey(String key) { 235 return getOverflowBubbleWithKey(key) != null; 236 } 237 238 @Nullable getSelectedBubble()239 public BubbleViewProvider getSelectedBubble() { 240 return mSelectedBubble; 241 } 242 getOverflow()243 public BubbleOverflow getOverflow() { 244 return mOverflow; 245 } 246 247 /** Return a read-only current active bubble lists. */ getActiveBubbles()248 public List<Bubble> getActiveBubbles() { 249 return Collections.unmodifiableList(mBubbles); 250 } 251 setExpanded(boolean expanded)252 public void setExpanded(boolean expanded) { 253 if (DEBUG_BUBBLE_DATA) { 254 Log.d(TAG, "setExpanded: " + expanded); 255 } 256 setExpandedInternal(expanded); 257 dispatchPendingChanges(); 258 } 259 setSelectedBubble(BubbleViewProvider bubble)260 public void setSelectedBubble(BubbleViewProvider bubble) { 261 if (DEBUG_BUBBLE_DATA) { 262 Log.d(TAG, "setSelectedBubble: " + bubble); 263 } 264 setSelectedBubbleInternal(bubble); 265 dispatchPendingChanges(); 266 } 267 setShowingOverflow(boolean showingOverflow)268 void setShowingOverflow(boolean showingOverflow) { 269 mShowingOverflow = showingOverflow; 270 } 271 isShowingOverflow()272 boolean isShowingOverflow() { 273 return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar()); 274 } 275 276 /** 277 * Constructs a new bubble or returns an existing one. Does not add new bubbles to 278 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)} 279 * for that. 280 * 281 * @param entry The notification entry to use, only null if it's a bubble being promoted from 282 * the overflow that was persisted over reboot. 283 * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from 284 * the overflow that was persisted over reboot. 285 */ getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble)286 public Bubble getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble) { 287 String key = persistedBubble != null ? persistedBubble.getKey() : entry.getKey(); 288 Bubble bubbleToReturn = getBubbleInStackWithKey(key); 289 290 if (bubbleToReturn == null) { 291 bubbleToReturn = getOverflowBubbleWithKey(key); 292 if (bubbleToReturn != null) { 293 // Promoting from overflow 294 mOverflowBubbles.remove(bubbleToReturn); 295 } else if (mPendingBubbles.containsKey(key)) { 296 // Update while it was pending 297 bubbleToReturn = mPendingBubbles.get(key); 298 } else if (entry != null) { 299 // New bubble 300 bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener, 301 mMainExecutor); 302 } else { 303 // Persisted bubble being promoted 304 bubbleToReturn = persistedBubble; 305 } 306 } 307 308 if (entry != null) { 309 bubbleToReturn.setEntry(entry); 310 } 311 mPendingBubbles.put(key, bubbleToReturn); 312 return bubbleToReturn; 313 } 314 315 /** 316 * When this method is called it is expected that all info in the bubble has completed loading. 317 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView, 318 * BubbleIconFactory, boolean) 319 */ notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade)320 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { 321 if (DEBUG_BUBBLE_DATA) { 322 Log.d(TAG, "notificationEntryUpdated: " + bubble); 323 } 324 mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here 325 Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); 326 suppressFlyout |= !bubble.isVisuallyInterruptive(); 327 328 if (prevBubble == null) { 329 // Create a new bubble 330 bubble.setSuppressFlyout(suppressFlyout); 331 doAdd(bubble); 332 trim(); 333 } else { 334 // Updates an existing bubble 335 bubble.setSuppressFlyout(suppressFlyout); 336 // If there is no flyout, we probably shouldn't show the bubble at the top 337 doUpdate(bubble, !suppressFlyout /* reorder */); 338 } 339 340 if (bubble.shouldAutoExpand()) { 341 bubble.setShouldAutoExpand(false); 342 setSelectedBubbleInternal(bubble); 343 if (!mExpanded) { 344 setExpandedInternal(true); 345 } 346 } 347 348 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble; 349 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade(); 350 bubble.setSuppressNotification(suppress); 351 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */); 352 353 LocusId locusId = bubble.getLocusId(); 354 if (locusId != null) { 355 boolean isSuppressed = mSuppressedBubbles.containsKey(locusId); 356 if (isSuppressed && (!bubble.isSuppressed() || !bubble.isSuppressable())) { 357 mSuppressedBubbles.remove(locusId); 358 mStateChange.unsuppressedBubble = bubble; 359 } else if (!isSuppressed && (bubble.isSuppressed() 360 || bubble.isSuppressable() && mVisibleLocusIds.contains(locusId))) { 361 mSuppressedBubbles.put(locusId, bubble); 362 mStateChange.suppressedBubble = bubble; 363 } 364 } 365 dispatchPendingChanges(); 366 } 367 368 /** 369 * Dismisses the bubble with the matching key, if it exists. 370 */ dismissBubbleWithKey(String key, @DismissReason int reason)371 public void dismissBubbleWithKey(String key, @DismissReason int reason) { 372 if (DEBUG_BUBBLE_DATA) { 373 Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason); 374 } 375 doRemove(key, reason); 376 dispatchPendingChanges(); 377 } 378 379 /** 380 * Adds a group key indicating that the summary for this group should be suppressed. 381 * 382 * @param groupKey the group key of the group whose summary should be suppressed. 383 * @param notifKey the notification entry key of that summary. 384 */ addSummaryToSuppress(String groupKey, String notifKey)385 void addSummaryToSuppress(String groupKey, String notifKey) { 386 mSuppressedGroupKeys.put(groupKey, notifKey); 387 mStateChange.suppressedSummaryChanged = true; 388 mStateChange.suppressedSummaryGroup = groupKey; 389 dispatchPendingChanges(); 390 } 391 392 /** 393 * Retrieves the notif entry key of the summary associated with the provided group key. 394 * 395 * @param groupKey the group to look up 396 * @return the key for the notification that is the summary of this group. 397 */ getSummaryKey(String groupKey)398 String getSummaryKey(String groupKey) { 399 return mSuppressedGroupKeys.get(groupKey); 400 } 401 402 /** 403 * Removes a group key indicating that summary for this group should no longer be suppressed. 404 */ removeSuppressedSummary(String groupKey)405 void removeSuppressedSummary(String groupKey) { 406 mSuppressedGroupKeys.remove(groupKey); 407 mStateChange.suppressedSummaryChanged = true; 408 mStateChange.suppressedSummaryGroup = groupKey; 409 dispatchPendingChanges(); 410 } 411 412 /** 413 * Whether the summary for the provided group key is suppressed. 414 */ 415 @VisibleForTesting isSummarySuppressed(String groupKey)416 public boolean isSummarySuppressed(String groupKey) { 417 return mSuppressedGroupKeys.containsKey(groupKey); 418 } 419 420 /** 421 * Removes bubbles from the given package whose shortcut are not in the provided list of valid 422 * shortcuts. 423 */ removeBubblesWithInvalidShortcuts( String packageName, List<ShortcutInfo> validShortcuts, int reason)424 public void removeBubblesWithInvalidShortcuts( 425 String packageName, List<ShortcutInfo> validShortcuts, int reason) { 426 427 final Set<String> validShortcutIds = new HashSet<String>(); 428 for (ShortcutInfo info : validShortcuts) { 429 validShortcutIds.add(info.getId()); 430 } 431 432 final Predicate<Bubble> invalidBubblesFromPackage = bubble -> { 433 final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName()); 434 final boolean isShortcutBubble = bubble.hasMetadataShortcutId(); 435 if (!bubbleIsFromPackage || !isShortcutBubble) { 436 return false; 437 } 438 final boolean hasShortcutIdAndValidShortcut = 439 bubble.hasMetadataShortcutId() 440 && bubble.getShortcutInfo() != null 441 && bubble.getShortcutInfo().isEnabled() 442 && validShortcutIds.contains(bubble.getShortcutInfo().getId()); 443 return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut; 444 }; 445 446 final Consumer<Bubble> removeBubble = bubble -> 447 dismissBubbleWithKey(bubble.getKey(), reason); 448 449 performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble); 450 performActionOnBubblesMatching( 451 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); 452 } 453 454 /** Dismisses all bubbles from the given package. */ removeBubblesWithPackageName(String packageName, int reason)455 public void removeBubblesWithPackageName(String packageName, int reason) { 456 final Predicate<Bubble> bubbleMatchesPackage = bubble -> 457 bubble.getPackageName().equals(packageName); 458 459 final Consumer<Bubble> removeBubble = bubble -> 460 dismissBubbleWithKey(bubble.getKey(), reason); 461 462 performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble); 463 performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); 464 } 465 doAdd(Bubble bubble)466 private void doAdd(Bubble bubble) { 467 if (DEBUG_BUBBLE_DATA) { 468 Log.d(TAG, "doAdd: " + bubble); 469 } 470 mBubbles.add(0, bubble); 471 mStateChange.addedBubble = bubble; 472 // Adding the first bubble doesn't change the order 473 mStateChange.orderChanged = mBubbles.size() > 1; 474 if (!isExpanded()) { 475 setSelectedBubbleInternal(mBubbles.get(0)); 476 } 477 } 478 trim()479 private void trim() { 480 if (mBubbles.size() > mMaxBubbles) { 481 int numtoRemove = mBubbles.size() - mMaxBubbles; 482 ArrayList<Bubble> toRemove = new ArrayList<>(); 483 mBubbles.stream() 484 // sort oldest first (ascending lastActivity) 485 .sorted(Comparator.comparingLong(Bubble::getLastActivity)) 486 // skip the selected bubble 487 .filter((b) -> !b.equals(mSelectedBubble)) 488 .forEachOrdered((b) -> { 489 if (toRemove.size() < numtoRemove) { 490 toRemove.add(b); 491 } 492 }); 493 toRemove.forEach((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED)); 494 } 495 } 496 doUpdate(Bubble bubble, boolean reorder)497 private void doUpdate(Bubble bubble, boolean reorder) { 498 if (DEBUG_BUBBLE_DATA) { 499 Log.d(TAG, "doUpdate: " + bubble); 500 } 501 mStateChange.updatedBubble = bubble; 502 if (!isExpanded() && reorder) { 503 int prevPos = mBubbles.indexOf(bubble); 504 mBubbles.remove(bubble); 505 mBubbles.add(0, bubble); 506 mStateChange.orderChanged = prevPos != 0; 507 setSelectedBubbleInternal(mBubbles.get(0)); 508 } 509 } 510 511 /** Runs the given action on Bubbles that match the given predicate. */ performActionOnBubblesMatching( List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action)512 private void performActionOnBubblesMatching( 513 List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) { 514 final List<Bubble> matchingBubbles = new ArrayList<>(); 515 for (Bubble bubble : bubbles) { 516 if (predicate.test(bubble)) { 517 matchingBubbles.add(bubble); 518 } 519 } 520 521 for (Bubble matchingBubble : matchingBubbles) { 522 action.accept(matchingBubble); 523 } 524 } 525 doRemove(String key, @DismissReason int reason)526 private void doRemove(String key, @DismissReason int reason) { 527 if (DEBUG_BUBBLE_DATA) { 528 Log.d(TAG, "doRemove: " + key); 529 } 530 // If it was pending remove it 531 if (mPendingBubbles.containsKey(key)) { 532 mPendingBubbles.remove(key); 533 } 534 int indexToRemove = indexForKey(key); 535 if (indexToRemove == -1) { 536 if (hasOverflowBubbleWithKey(key) 537 && (reason == Bubbles.DISMISS_NOTIF_CANCEL 538 || reason == Bubbles.DISMISS_GROUP_CANCELLED 539 || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE 540 || reason == Bubbles.DISMISS_BLOCKED 541 || reason == Bubbles.DISMISS_SHORTCUT_REMOVED 542 || reason == Bubbles.DISMISS_PACKAGE_REMOVED 543 || reason == Bubbles.DISMISS_USER_CHANGED)) { 544 545 Bubble b = getOverflowBubbleWithKey(key); 546 if (DEBUG_BUBBLE_DATA) { 547 Log.d(TAG, "Cancel overflow bubble: " + b); 548 } 549 if (b != null) { 550 b.stopInflation(); 551 } 552 mLogger.logOverflowRemove(b, reason); 553 mOverflowBubbles.remove(b); 554 mStateChange.bubbleRemoved(b, reason); 555 mStateChange.removedOverflowBubble = b; 556 } 557 return; 558 } 559 Bubble bubbleToRemove = mBubbles.get(indexToRemove); 560 bubbleToRemove.stopInflation(); 561 if (mBubbles.size() == 1) { 562 if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) { 563 // No more active bubbles but we have stuff in the overflow -- select that view 564 // if we're already expanded or always showing. 565 setShowingOverflow(true); 566 setSelectedBubbleInternal(mOverflow); 567 } else { 568 setExpandedInternal(false); 569 // Don't use setSelectedBubbleInternal because we don't want to trigger an 570 // applyUpdate 571 mSelectedBubble = null; 572 } 573 } 574 if (indexToRemove < mBubbles.size() - 1) { 575 // Removing anything but the last bubble means positions will change. 576 mStateChange.orderChanged = true; 577 } 578 mBubbles.remove(indexToRemove); 579 mStateChange.bubbleRemoved(bubbleToRemove, reason); 580 if (!isExpanded()) { 581 mStateChange.orderChanged |= repackAll(); 582 } 583 584 overflowBubble(reason, bubbleToRemove); 585 586 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. 587 if (Objects.equals(mSelectedBubble, bubbleToRemove)) { 588 // Move selection to the new bubble at the same position. 589 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); 590 BubbleViewProvider newSelected = mBubbles.get(newIndex); 591 setSelectedBubbleInternal(newSelected); 592 } 593 maybeSendDeleteIntent(reason, bubbleToRemove); 594 } 595 overflowBubble(@ismissReason int reason, Bubble bubble)596 void overflowBubble(@DismissReason int reason, Bubble bubble) { 597 if (bubble.getPendingIntentCanceled() 598 || !(reason == Bubbles.DISMISS_AGED 599 || reason == Bubbles.DISMISS_USER_GESTURE 600 || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { 601 return; 602 } 603 if (DEBUG_BUBBLE_DATA) { 604 Log.d(TAG, "Overflowing: " + bubble); 605 } 606 mLogger.logOverflowAdd(bubble, reason); 607 mOverflowBubbles.remove(bubble); 608 mOverflowBubbles.add(0, bubble); 609 mStateChange.addedOverflowBubble = bubble; 610 bubble.stopInflation(); 611 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { 612 // Remove oldest bubble. 613 Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1); 614 if (DEBUG_BUBBLE_DATA) { 615 Log.d(TAG, "Overflow full. Remove: " + oldest); 616 } 617 mStateChange.bubbleRemoved(oldest, Bubbles.DISMISS_OVERFLOW_MAX_REACHED); 618 mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED); 619 mOverflowBubbles.remove(oldest); 620 mStateChange.removedOverflowBubble = oldest; 621 } 622 } 623 dismissAll(@ismissReason int reason)624 public void dismissAll(@DismissReason int reason) { 625 if (DEBUG_BUBBLE_DATA) { 626 Log.d(TAG, "dismissAll: reason=" + reason); 627 } 628 if (mBubbles.isEmpty()) { 629 return; 630 } 631 setExpandedInternal(false); 632 setSelectedBubbleInternal(null); 633 while (!mBubbles.isEmpty()) { 634 doRemove(mBubbles.get(0).getKey(), reason); 635 } 636 dispatchPendingChanges(); 637 } 638 639 /** 640 * Called in response to the visibility of a locusId changing. A locusId is set on a task 641 * and if there's a matching bubble for that locusId then the bubble may be hidden or shown 642 * depending on the visibility of the locusId. 643 * 644 * @param taskId the taskId associated with the locusId visibility change. 645 * @param locusId the locusId whose visibility has changed. 646 * @param visible whether the task with the locusId is visible or not. 647 */ onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible)648 public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) { 649 Bubble matchingBubble = getBubbleInStackWithLocusId(locusId); 650 // Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled. 651 if (visible && (matchingBubble == null || matchingBubble.getTaskId() != taskId)) { 652 mVisibleLocusIds.add(locusId); 653 } else { 654 mVisibleLocusIds.remove(locusId); 655 } 656 if (matchingBubble == null) { 657 return; 658 } 659 boolean isAlreadySuppressed = mSuppressedBubbles.get(locusId) != null; 660 if (visible && !isAlreadySuppressed && matchingBubble.isSuppressable() 661 && taskId != matchingBubble.getTaskId()) { 662 mSuppressedBubbles.put(locusId, matchingBubble); 663 matchingBubble.setSuppressBubble(true); 664 mStateChange.suppressedBubble = matchingBubble; 665 dispatchPendingChanges(); 666 } else if (!visible) { 667 Bubble unsuppressedBubble = mSuppressedBubbles.remove(locusId); 668 if (unsuppressedBubble != null) { 669 unsuppressedBubble.setSuppressBubble(false); 670 mStateChange.unsuppressedBubble = unsuppressedBubble; 671 } 672 dispatchPendingChanges(); 673 } 674 } 675 676 /** 677 * Removes all bubbles from the overflow, called when the user changes. 678 */ clearOverflow()679 public void clearOverflow() { 680 while (!mOverflowBubbles.isEmpty()) { 681 doRemove(mOverflowBubbles.get(0).getKey(), Bubbles.DISMISS_USER_CHANGED); 682 } 683 dispatchPendingChanges(); 684 } 685 dispatchPendingChanges()686 private void dispatchPendingChanges() { 687 if (mListener != null && mStateChange.anythingChanged()) { 688 mListener.applyUpdate(mStateChange); 689 } 690 mStateChange = new Update(mBubbles, mOverflowBubbles); 691 } 692 693 /** 694 * Requests a change to the selected bubble. 695 * 696 * @param bubble the new selected bubble 697 */ setSelectedBubbleInternal(@ullable BubbleViewProvider bubble)698 private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) { 699 if (DEBUG_BUBBLE_DATA) { 700 Log.d(TAG, "setSelectedBubbleInternal: " + bubble); 701 } 702 if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { 703 return; 704 } 705 // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. 706 boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey()); 707 if (bubble != null 708 && !mBubbles.contains(bubble) 709 && !mOverflowBubbles.contains(bubble) 710 && !isOverflow) { 711 Log.e(TAG, "Cannot select bubble which doesn't exist!" 712 + " (" + bubble + ") bubbles=" + mBubbles); 713 return; 714 } 715 if (mExpanded && bubble != null && !isOverflow) { 716 ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); 717 } 718 mSelectedBubble = bubble; 719 mStateChange.selectedBubble = bubble; 720 mStateChange.selectionChanged = true; 721 } 722 setCurrentUserId(int uid)723 void setCurrentUserId(int uid) { 724 mCurrentUserId = uid; 725 } 726 727 /** 728 * Logs the bubble UI event. 729 * 730 * @param provider The bubble view provider that is being interacted on. Null value indicates 731 * that the user interaction is not specific to one bubble. 732 * @param action The user interaction enum 733 * @param packageName SystemUI package 734 * @param bubbleCount Number of bubbles in the stack 735 * @param bubbleIndex Index of bubble in the stack 736 * @param normalX Normalized x position of the stack 737 * @param normalY Normalized y position of the stack 738 */ logBubbleEvent(@ullable BubbleViewProvider provider, int action, String packageName, int bubbleCount, int bubbleIndex, float normalX, float normalY)739 void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, 740 int bubbleCount, int bubbleIndex, float normalX, float normalY) { 741 if (provider == null) { 742 mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY); 743 } else if (provider.getKey().equals(BubbleOverflow.KEY)) { 744 if (action == FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) { 745 mLogger.logShowOverflow(packageName, mCurrentUserId); 746 } 747 } else { 748 mLogger.logBubbleUiChanged((Bubble) provider, packageName, action, bubbleCount, normalX, 749 normalY, bubbleIndex); 750 } 751 } 752 753 /** 754 * Requests a change to the expanded state. 755 * 756 * @param shouldExpand the new requested state 757 */ setExpandedInternal(boolean shouldExpand)758 private void setExpandedInternal(boolean shouldExpand) { 759 if (DEBUG_BUBBLE_DATA) { 760 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); 761 } 762 if (mExpanded == shouldExpand) { 763 return; 764 } 765 if (shouldExpand) { 766 if (mBubbles.isEmpty() && !mShowingOverflow) { 767 Log.e(TAG, "Attempt to expand stack when empty!"); 768 return; 769 } 770 if (mSelectedBubble == null) { 771 Log.e(TAG, "Attempt to expand stack without selected bubble!"); 772 return; 773 } 774 if (mSelectedBubble instanceof Bubble) { 775 ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); 776 } 777 mStateChange.orderChanged |= repackAll(); 778 } else if (!mBubbles.isEmpty()) { 779 // Apply ordering and grouping rules from expanded -> collapsed, then save 780 // the result. 781 mStateChange.orderChanged |= repackAll(); 782 // Save the state which should be returned to when expanded (with no other changes) 783 784 if (mShowingOverflow) { 785 // Show previously selected bubble instead of overflow menu on next expansion. 786 if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) { 787 setSelectedBubbleInternal(mSelectedBubble); 788 } else { 789 setSelectedBubbleInternal(mBubbles.get(0)); 790 } 791 } 792 if (mBubbles.indexOf(mSelectedBubble) > 0) { 793 // Move the selected bubble to the top while collapsed. 794 int index = mBubbles.indexOf(mSelectedBubble); 795 if (index != 0) { 796 mBubbles.remove((Bubble) mSelectedBubble); 797 mBubbles.add(0, (Bubble) mSelectedBubble); 798 mStateChange.orderChanged = true; 799 } 800 } 801 } 802 if (mNeedsTrimming) { 803 mNeedsTrimming = false; 804 trim(); 805 } 806 mExpanded = shouldExpand; 807 mStateChange.expanded = shouldExpand; 808 mStateChange.expandedChanged = true; 809 } 810 sortKey(Bubble bubble)811 private static long sortKey(Bubble bubble) { 812 return bubble.getLastActivity(); 813 } 814 815 /** 816 * This applies a full sort and group pass to all existing bubbles. 817 * Bubbles are sorted by lastUpdated descending. 818 * 819 * @return true if the position of any bubbles changed as a result 820 */ repackAll()821 private boolean repackAll() { 822 if (DEBUG_BUBBLE_DATA) { 823 Log.d(TAG, "repackAll()"); 824 } 825 if (mBubbles.isEmpty()) { 826 return false; 827 } 828 List<Bubble> repacked = new ArrayList<>(mBubbles.size()); 829 // Add bubbles, freshest to oldest 830 mBubbles.stream() 831 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) 832 .forEachOrdered(repacked::add); 833 if (repacked.equals(mBubbles)) { 834 return false; 835 } 836 mBubbles.clear(); 837 mBubbles.addAll(repacked); 838 return true; 839 } 840 maybeSendDeleteIntent(@ismissReason int reason, @NonNull final Bubble bubble)841 private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) { 842 if (reason != Bubbles.DISMISS_USER_GESTURE) return; 843 PendingIntent deleteIntent = bubble.getDeleteIntent(); 844 if (deleteIntent == null) return; 845 try { 846 deleteIntent.send(); 847 } catch (PendingIntent.CanceledException e) { 848 Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey()); 849 } 850 } 851 indexForKey(String key)852 private int indexForKey(String key) { 853 for (int i = 0; i < mBubbles.size(); i++) { 854 Bubble bubble = mBubbles.get(i); 855 if (bubble.getKey().equals(key)) { 856 return i; 857 } 858 } 859 return -1; 860 } 861 862 /** 863 * The set of bubbles in row. 864 */ 865 @VisibleForTesting(visibility = PACKAGE) getBubbles()866 public List<Bubble> getBubbles() { 867 return Collections.unmodifiableList(mBubbles); 868 } 869 870 /** 871 * The set of bubbles in overflow. 872 */ 873 @VisibleForTesting(visibility = PRIVATE) getOverflowBubbles()874 public List<Bubble> getOverflowBubbles() { 875 return Collections.unmodifiableList(mOverflowBubbles); 876 } 877 878 @VisibleForTesting(visibility = PRIVATE) 879 @Nullable getAnyBubbleWithkey(String key)880 Bubble getAnyBubbleWithkey(String key) { 881 Bubble b = getBubbleInStackWithKey(key); 882 if (b == null) { 883 b = getOverflowBubbleWithKey(key); 884 } 885 return b; 886 } 887 888 /** @return any bubble (in the stack or the overflow) that matches the provided shortcutId. */ 889 @Nullable getAnyBubbleWithShortcutId(String shortcutId)890 Bubble getAnyBubbleWithShortcutId(String shortcutId) { 891 if (TextUtils.isEmpty(shortcutId)) { 892 return null; 893 } 894 for (int i = 0; i < mBubbles.size(); i++) { 895 Bubble bubble = mBubbles.get(i); 896 String bubbleShortcutId = bubble.getShortcutInfo() != null 897 ? bubble.getShortcutInfo().getId() 898 : bubble.getMetadataShortcutId(); 899 if (shortcutId.equals(bubbleShortcutId)) { 900 return bubble; 901 } 902 } 903 904 for (int i = 0; i < mOverflowBubbles.size(); i++) { 905 Bubble bubble = mOverflowBubbles.get(i); 906 String bubbleShortcutId = bubble.getShortcutInfo() != null 907 ? bubble.getShortcutInfo().getId() 908 : bubble.getMetadataShortcutId(); 909 if (shortcutId.equals(bubbleShortcutId)) { 910 return bubble; 911 } 912 } 913 return null; 914 } 915 916 @VisibleForTesting(visibility = PRIVATE) 917 @Nullable getBubbleInStackWithKey(String key)918 public Bubble getBubbleInStackWithKey(String key) { 919 for (int i = 0; i < mBubbles.size(); i++) { 920 Bubble bubble = mBubbles.get(i); 921 if (bubble.getKey().equals(key)) { 922 return bubble; 923 } 924 } 925 return null; 926 } 927 928 @Nullable getBubbleInStackWithLocusId(LocusId locusId)929 private Bubble getBubbleInStackWithLocusId(LocusId locusId) { 930 if (locusId == null) return null; 931 for (int i = 0; i < mBubbles.size(); i++) { 932 Bubble bubble = mBubbles.get(i); 933 if (locusId.equals(bubble.getLocusId())) { 934 return bubble; 935 } 936 } 937 return null; 938 } 939 940 @Nullable getBubbleWithView(View view)941 Bubble getBubbleWithView(View view) { 942 for (int i = 0; i < mBubbles.size(); i++) { 943 Bubble bubble = mBubbles.get(i); 944 if (bubble.getIconView() != null && bubble.getIconView().equals(view)) { 945 return bubble; 946 } 947 } 948 return null; 949 } 950 951 @VisibleForTesting(visibility = PRIVATE) getOverflowBubbleWithKey(String key)952 public Bubble getOverflowBubbleWithKey(String key) { 953 for (int i = 0; i < mOverflowBubbles.size(); i++) { 954 Bubble bubble = mOverflowBubbles.get(i); 955 if (bubble.getKey().equals(key)) { 956 return bubble; 957 } 958 } 959 return null; 960 } 961 962 @VisibleForTesting(visibility = PRIVATE) setTimeSource(TimeSource timeSource)963 void setTimeSource(TimeSource timeSource) { 964 mTimeSource = timeSource; 965 } 966 setListener(Listener listener)967 public void setListener(Listener listener) { 968 mListener = listener; 969 } 970 971 /** 972 * Set maximum number of bubbles allowed in overflow. 973 * This method should only be used in tests, not in production. 974 */ 975 @VisibleForTesting setMaxOverflowBubbles(int maxOverflowBubbles)976 public void setMaxOverflowBubbles(int maxOverflowBubbles) { 977 mMaxOverflowBubbles = maxOverflowBubbles; 978 } 979 980 /** 981 * Description of current bubble data state. 982 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)983 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 984 pw.print("selected: "); 985 pw.println(mSelectedBubble != null 986 ? mSelectedBubble.getKey() 987 : "null"); 988 pw.print("expanded: "); 989 pw.println(mExpanded); 990 991 pw.print("stack bubble count: "); 992 pw.println(mBubbles.size()); 993 for (Bubble bubble : mBubbles) { 994 bubble.dump(fd, pw, args); 995 } 996 997 pw.print("overflow bubble count: "); 998 pw.println(mOverflowBubbles.size()); 999 for (Bubble bubble : mOverflowBubbles) { 1000 bubble.dump(fd, pw, args); 1001 } 1002 1003 pw.print("summaryKeys: "); 1004 pw.println(mSuppressedGroupKeys.size()); 1005 for (String key : mSuppressedGroupKeys.keySet()) { 1006 pw.println(" suppressing: " + key); 1007 } 1008 } 1009 } 1010