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.launcher3.icons.BubbleIconFactory; 41 import com.android.wm.shell.R; 42 import com.android.wm.shell.bubbles.Bubbles.DismissReason; 43 import com.android.wm.shell.common.bubbles.BubbleBarUpdate; 44 import com.android.wm.shell.common.bubbles.RemovedBubble; 45 46 import java.io.PrintWriter; 47 import java.util.ArrayList; 48 import java.util.Collections; 49 import java.util.Comparator; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.List; 53 import java.util.Objects; 54 import java.util.Set; 55 import java.util.concurrent.Executor; 56 import java.util.function.Consumer; 57 import java.util.function.Predicate; 58 59 /** 60 * Keeps track of active bubbles. 61 */ 62 public class BubbleData { 63 64 private BubbleLogger mLogger; 65 66 private int mCurrentUserId; 67 68 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; 69 70 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = 71 Comparator.comparing(BubbleData::sortKey).reversed(); 72 73 /** Contains information about changes that have been made to the state of bubbles. */ 74 static final class Update { 75 boolean expandedChanged; 76 boolean selectionChanged; 77 boolean orderChanged; 78 boolean suppressedSummaryChanged; 79 boolean expanded; 80 @Nullable BubbleViewProvider selectedBubble; 81 @Nullable Bubble addedBubble; 82 @Nullable Bubble updatedBubble; 83 @Nullable Bubble addedOverflowBubble; 84 @Nullable Bubble removedOverflowBubble; 85 @Nullable Bubble suppressedBubble; 86 @Nullable Bubble unsuppressedBubble; 87 @Nullable String suppressedSummaryGroup; 88 // Pair with Bubble and @DismissReason Integer 89 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); 90 91 // A read-only view of the bubbles list, changes there will be reflected here. 92 final List<Bubble> bubbles; 93 final List<Bubble> overflowBubbles; 94 Update(List<Bubble> row, List<Bubble> overflow)95 private Update(List<Bubble> row, List<Bubble> overflow) { 96 bubbles = Collections.unmodifiableList(row); 97 overflowBubbles = Collections.unmodifiableList(overflow); 98 } 99 anythingChanged()100 boolean anythingChanged() { 101 return expandedChanged 102 || selectionChanged 103 || addedBubble != null 104 || updatedBubble != null 105 || !removedBubbles.isEmpty() 106 || addedOverflowBubble != null 107 || removedOverflowBubble != null 108 || orderChanged 109 || suppressedBubble != null 110 || unsuppressedBubble != null 111 || suppressedSummaryChanged 112 || suppressedSummaryGroup != null; 113 } 114 bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason)115 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { 116 removedBubbles.add(new Pair<>(bubbleToRemove, reason)); 117 } 118 119 /** 120 * Converts the update to a {@link BubbleBarUpdate} which contains updates relevant 121 * to the bubble bar. Only used when {@link BubbleController#isShowingAsBubbleBar()} is 122 * true. 123 */ toBubbleBarUpdate()124 BubbleBarUpdate toBubbleBarUpdate() { 125 BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); 126 127 bubbleBarUpdate.expandedChanged = expandedChanged; 128 bubbleBarUpdate.expanded = expanded; 129 if (selectionChanged) { 130 bubbleBarUpdate.selectedBubbleKey = selectedBubble != null 131 ? selectedBubble.getKey() 132 : null; 133 } 134 bubbleBarUpdate.addedBubble = addedBubble != null 135 ? addedBubble.asBubbleBarBubble() 136 : null; 137 // TODO(b/269670235): We need to handle updates better, I think for the bubble bar only 138 // certain updates need to be sent instead of any updatedBubble. 139 bubbleBarUpdate.updatedBubble = updatedBubble != null 140 ? updatedBubble.asBubbleBarBubble() 141 : null; 142 bubbleBarUpdate.suppressedBubbleKey = suppressedBubble != null 143 ? suppressedBubble.getKey() 144 : null; 145 bubbleBarUpdate.unsupressedBubbleKey = unsuppressedBubble != null 146 ? unsuppressedBubble.getKey() 147 : null; 148 for (int i = 0; i < removedBubbles.size(); i++) { 149 Pair<Bubble, Integer> pair = removedBubbles.get(i); 150 bubbleBarUpdate.removedBubbles.add( 151 new RemovedBubble(pair.first.getKey(), pair.second)); 152 } 153 if (orderChanged) { 154 // Include the new order 155 for (int i = 0; i < bubbles.size(); i++) { 156 bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey()); 157 } 158 } 159 return bubbleBarUpdate; 160 } 161 162 /** 163 * Gets the current state of active bubbles and populates the update with that. Only 164 * used when {@link BubbleController#isShowingAsBubbleBar()} is true. 165 */ getInitialState()166 BubbleBarUpdate getInitialState() { 167 BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); 168 for (int i = 0; i < bubbles.size(); i++) { 169 bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); 170 } 171 return bubbleBarUpdate; 172 } 173 } 174 175 /** 176 * This interface reports changes to the state and appearance of bubbles which should be applied 177 * as necessary to the UI. 178 */ 179 interface Listener { 180 /** Reports changes have have occurred as a result of the most recent operation. */ applyUpdate(Update update)181 void applyUpdate(Update update); 182 } 183 184 interface TimeSource { currentTimeMillis()185 long currentTimeMillis(); 186 } 187 188 private final Context mContext; 189 private final BubblePositioner mPositioner; 190 private final Executor mMainExecutor; 191 /** Bubbles that are actively in the stack. */ 192 private final List<Bubble> mBubbles; 193 /** Bubbles that aged out to overflow. */ 194 private final List<Bubble> mOverflowBubbles; 195 /** Bubbles that are being loaded but haven't been added to the stack just yet. */ 196 private final HashMap<String, Bubble> mPendingBubbles; 197 /** Bubbles that are suppressed due to locusId. */ 198 private final ArrayMap<LocusId, Bubble> mSuppressedBubbles = new ArrayMap<>(); 199 /** Visible locusIds. */ 200 private final ArraySet<LocusId> mVisibleLocusIds = new ArraySet<>(); 201 202 private BubbleViewProvider mSelectedBubble; 203 private final BubbleOverflow mOverflow; 204 private boolean mShowingOverflow; 205 private boolean mExpanded; 206 private int mMaxBubbles; 207 private int mMaxOverflowBubbles; 208 209 private boolean mNeedsTrimming; 210 211 // State tracked during an operation -- keeps track of what listener events to dispatch. 212 private Update mStateChange; 213 214 private TimeSource mTimeSource = System::currentTimeMillis; 215 216 @Nullable 217 private Listener mListener; 218 219 private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; 220 private Bubbles.PendingIntentCanceledListener mCancelledListener; 221 222 /** 223 * We track groups with summaries that aren't visibly displayed but still kept around because 224 * the bubble(s) associated with the summary still exist. 225 * 226 * The summary must be kept around so that developers can cancel it (and hence the bubbles 227 * associated with it). This list is used to check if the summary should be hidden from the 228 * shade. 229 * 230 * Key: group key of the notification 231 * Value: key of the notification 232 */ 233 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); 234 BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, Executor mainExecutor)235 public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, 236 Executor mainExecutor) { 237 mContext = context; 238 mLogger = bubbleLogger; 239 mPositioner = positioner; 240 mMainExecutor = mainExecutor; 241 mOverflow = new BubbleOverflow(context, positioner); 242 mBubbles = new ArrayList<>(); 243 mOverflowBubbles = new ArrayList<>(); 244 mPendingBubbles = new HashMap<>(); 245 mStateChange = new Update(mBubbles, mOverflowBubbles); 246 mMaxBubbles = mPositioner.getMaxBubbles(); 247 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); 248 } 249 250 /** 251 * Returns a bubble bar update populated with the current list of active bubbles. 252 */ getInitialStateForBubbleBar()253 public BubbleBarUpdate getInitialStateForBubbleBar() { 254 return mStateChange.getInitialState(); 255 } 256 setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener)257 public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { 258 mBubbleMetadataFlagListener = listener; 259 } 260 setPendingIntentCancelledListener( Bubbles.PendingIntentCanceledListener listener)261 public void setPendingIntentCancelledListener( 262 Bubbles.PendingIntentCanceledListener listener) { 263 mCancelledListener = listener; 264 } 265 onMaxBubblesChanged()266 public void onMaxBubblesChanged() { 267 mMaxBubbles = mPositioner.getMaxBubbles(); 268 if (!mExpanded) { 269 trim(); 270 dispatchPendingChanges(); 271 } else { 272 mNeedsTrimming = true; 273 } 274 } 275 hasBubbles()276 public boolean hasBubbles() { 277 return !mBubbles.isEmpty(); 278 } 279 hasOverflowBubbles()280 public boolean hasOverflowBubbles() { 281 return !mOverflowBubbles.isEmpty(); 282 } 283 isExpanded()284 public boolean isExpanded() { 285 return mExpanded; 286 } 287 hasAnyBubbleWithKey(String key)288 public boolean hasAnyBubbleWithKey(String key) { 289 return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key) 290 || hasSuppressedBubbleWithKey(key); 291 } 292 hasBubbleInStackWithKey(String key)293 public boolean hasBubbleInStackWithKey(String key) { 294 return getBubbleInStackWithKey(key) != null; 295 } 296 hasOverflowBubbleWithKey(String key)297 public boolean hasOverflowBubbleWithKey(String key) { 298 return getOverflowBubbleWithKey(key) != null; 299 } 300 301 /** 302 * Check if there are any bubbles suppressed with the given notification <code>key</code> 303 */ hasSuppressedBubbleWithKey(String key)304 public boolean hasSuppressedBubbleWithKey(String key) { 305 return mSuppressedBubbles.values().stream().anyMatch(b -> b.getKey().equals(key)); 306 } 307 308 /** 309 * Check if there are any bubbles suppressed with the given <code>LocusId</code> 310 */ isSuppressedWithLocusId(LocusId locusId)311 public boolean isSuppressedWithLocusId(LocusId locusId) { 312 return mSuppressedBubbles.get(locusId) != null; 313 } 314 315 @Nullable getSelectedBubble()316 public BubbleViewProvider getSelectedBubble() { 317 return mSelectedBubble; 318 } 319 getOverflow()320 public BubbleOverflow getOverflow() { 321 return mOverflow; 322 } 323 324 /** Return a read-only current active bubble lists. */ getActiveBubbles()325 public List<Bubble> getActiveBubbles() { 326 return Collections.unmodifiableList(mBubbles); 327 } 328 setExpanded(boolean expanded)329 public void setExpanded(boolean expanded) { 330 if (DEBUG_BUBBLE_DATA) { 331 Log.d(TAG, "setExpanded: " + expanded); 332 } 333 setExpandedInternal(expanded); 334 dispatchPendingChanges(); 335 } 336 337 /** 338 * Sets the selected bubble and expands it, but doesn't dispatch changes 339 * to {@link BubbleData.Listener}. This is used for updates coming from launcher whose views 340 * will already be updated so we don't need to notify them again, but BubbleData should be 341 * updated to have the correct state. 342 */ setSelectedBubbleFromLauncher(BubbleViewProvider bubble)343 public void setSelectedBubbleFromLauncher(BubbleViewProvider bubble) { 344 if (DEBUG_BUBBLE_DATA) { 345 Log.d(TAG, "setSelectedBubbleFromLauncher: " + bubble); 346 } 347 mExpanded = true; 348 if (Objects.equals(bubble, mSelectedBubble)) { 349 return; 350 } 351 boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey()); 352 if (bubble != null 353 && !mBubbles.contains(bubble) 354 && !mOverflowBubbles.contains(bubble) 355 && !isOverflow) { 356 Log.e(TAG, "Cannot select bubble which doesn't exist!" 357 + " (" + bubble + ") bubbles=" + mBubbles); 358 return; 359 } 360 if (bubble != null && !isOverflow) { 361 ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); 362 } 363 mSelectedBubble = bubble; 364 } 365 setSelectedBubble(BubbleViewProvider bubble)366 public void setSelectedBubble(BubbleViewProvider bubble) { 367 if (DEBUG_BUBBLE_DATA) { 368 Log.d(TAG, "setSelectedBubble: " + bubble); 369 } 370 setSelectedBubbleInternal(bubble); 371 dispatchPendingChanges(); 372 } 373 setShowingOverflow(boolean showingOverflow)374 void setShowingOverflow(boolean showingOverflow) { 375 mShowingOverflow = showingOverflow; 376 } 377 isShowingOverflow()378 boolean isShowingOverflow() { 379 return mShowingOverflow && isExpanded(); 380 } 381 382 /** 383 * Constructs a new bubble or returns an existing one. Does not add new bubbles to 384 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)} 385 * for that. 386 * 387 * @param entry The notification entry to use, only null if it's a bubble being promoted from 388 * the overflow that was persisted over reboot. 389 * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from 390 * the overflow that was persisted over reboot. 391 */ getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble)392 public Bubble getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble) { 393 String key = persistedBubble != null ? persistedBubble.getKey() : entry.getKey(); 394 Bubble bubbleToReturn = getBubbleInStackWithKey(key); 395 396 if (bubbleToReturn == null) { 397 bubbleToReturn = getOverflowBubbleWithKey(key); 398 if (bubbleToReturn != null) { 399 // Promoting from overflow 400 mOverflowBubbles.remove(bubbleToReturn); 401 } else if (mPendingBubbles.containsKey(key)) { 402 // Update while it was pending 403 bubbleToReturn = mPendingBubbles.get(key); 404 } else if (entry != null) { 405 // New bubble 406 bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, 407 mMainExecutor); 408 } else { 409 // Persisted bubble being promoted 410 bubbleToReturn = persistedBubble; 411 } 412 } 413 414 if (entry != null) { 415 bubbleToReturn.setEntry(entry); 416 } 417 mPendingBubbles.put(key, bubbleToReturn); 418 return bubbleToReturn; 419 } 420 421 /** 422 * When this method is called it is expected that all info in the bubble has completed loading. 423 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView, 424 * BubbleIconFactory, boolean) 425 */ notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade)426 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { 427 if (DEBUG_BUBBLE_DATA) { 428 Log.d(TAG, "notificationEntryUpdated: " + bubble); 429 } 430 mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here 431 Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); 432 suppressFlyout |= !bubble.isTextChanged(); 433 434 if (prevBubble == null) { 435 // Create a new bubble 436 bubble.setSuppressFlyout(suppressFlyout); 437 bubble.markUpdatedAt(mTimeSource.currentTimeMillis()); 438 doAdd(bubble); 439 trim(); 440 } else { 441 // Updates an existing bubble 442 bubble.setSuppressFlyout(suppressFlyout); 443 // If there is no flyout, we probably shouldn't show the bubble at the top 444 doUpdate(bubble, !suppressFlyout /* reorder */); 445 } 446 447 if (bubble.shouldAutoExpand()) { 448 bubble.setShouldAutoExpand(false); 449 setSelectedBubbleInternal(bubble); 450 if (!mExpanded) { 451 setExpandedInternal(true); 452 } 453 } 454 455 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble; 456 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade(); 457 bubble.setSuppressNotification(suppress); 458 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */); 459 460 LocusId locusId = bubble.getLocusId(); 461 if (locusId != null) { 462 boolean isSuppressed = mSuppressedBubbles.containsKey(locusId); 463 if (isSuppressed && (!bubble.isSuppressed() || !bubble.isSuppressable())) { 464 mSuppressedBubbles.remove(locusId); 465 doUnsuppress(bubble); 466 } else if (!isSuppressed && (bubble.isSuppressed() 467 || bubble.isSuppressable() && mVisibleLocusIds.contains(locusId))) { 468 mSuppressedBubbles.put(locusId, bubble); 469 doSuppress(bubble); 470 } 471 } 472 dispatchPendingChanges(); 473 } 474 475 /** 476 * Dismisses the bubble with the matching key, if it exists. 477 */ dismissBubbleWithKey(String key, @DismissReason int reason)478 public void dismissBubbleWithKey(String key, @DismissReason int reason) { 479 if (DEBUG_BUBBLE_DATA) { 480 Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason); 481 } 482 doRemove(key, reason); 483 dispatchPendingChanges(); 484 } 485 486 /** 487 * Adds a group key indicating that the summary for this group should be suppressed. 488 * 489 * @param groupKey the group key of the group whose summary should be suppressed. 490 * @param notifKey the notification entry key of that summary. 491 */ addSummaryToSuppress(String groupKey, String notifKey)492 void addSummaryToSuppress(String groupKey, String notifKey) { 493 mSuppressedGroupKeys.put(groupKey, notifKey); 494 mStateChange.suppressedSummaryChanged = true; 495 mStateChange.suppressedSummaryGroup = groupKey; 496 dispatchPendingChanges(); 497 } 498 499 /** 500 * Retrieves the notif entry key of the summary associated with the provided group key. 501 * 502 * @param groupKey the group to look up 503 * @return the key for the notification that is the summary of this group. 504 */ getSummaryKey(String groupKey)505 String getSummaryKey(String groupKey) { 506 return mSuppressedGroupKeys.get(groupKey); 507 } 508 509 /** 510 * Removes a group key indicating that summary for this group should no longer be suppressed. 511 */ removeSuppressedSummary(String groupKey)512 void removeSuppressedSummary(String groupKey) { 513 mSuppressedGroupKeys.remove(groupKey); 514 mStateChange.suppressedSummaryChanged = true; 515 mStateChange.suppressedSummaryGroup = groupKey; 516 dispatchPendingChanges(); 517 } 518 519 /** 520 * Whether the summary for the provided group key is suppressed. 521 */ 522 @VisibleForTesting isSummarySuppressed(String groupKey)523 public boolean isSummarySuppressed(String groupKey) { 524 return mSuppressedGroupKeys.containsKey(groupKey); 525 } 526 527 /** 528 * Removes bubbles from the given package whose shortcut are not in the provided list of valid 529 * shortcuts. 530 */ removeBubblesWithInvalidShortcuts( String packageName, List<ShortcutInfo> validShortcuts, int reason)531 public void removeBubblesWithInvalidShortcuts( 532 String packageName, List<ShortcutInfo> validShortcuts, int reason) { 533 534 final Set<String> validShortcutIds = new HashSet<String>(); 535 for (ShortcutInfo info : validShortcuts) { 536 validShortcutIds.add(info.getId()); 537 } 538 539 final Predicate<Bubble> invalidBubblesFromPackage = bubble -> { 540 final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName()); 541 final boolean isShortcutBubble = bubble.hasMetadataShortcutId(); 542 if (!bubbleIsFromPackage || !isShortcutBubble) { 543 return false; 544 } 545 final boolean hasShortcutIdAndValidShortcut = 546 bubble.hasMetadataShortcutId() 547 && bubble.getShortcutInfo() != null 548 && bubble.getShortcutInfo().isEnabled() 549 && validShortcutIds.contains(bubble.getShortcutInfo().getId()); 550 return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut; 551 }; 552 553 final Consumer<Bubble> removeBubble = bubble -> 554 dismissBubbleWithKey(bubble.getKey(), reason); 555 556 performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble); 557 performActionOnBubblesMatching( 558 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); 559 } 560 561 /** Removes all bubbles from the given package. */ removeBubblesWithPackageName(String packageName, int reason)562 public void removeBubblesWithPackageName(String packageName, int reason) { 563 final Predicate<Bubble> bubbleMatchesPackage = bubble -> 564 bubble.getPackageName().equals(packageName); 565 566 final Consumer<Bubble> removeBubble = bubble -> 567 dismissBubbleWithKey(bubble.getKey(), reason); 568 569 performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble); 570 performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); 571 } 572 573 /** Removes all bubbles for the given user. */ removeBubblesForUser(int userId)574 public void removeBubblesForUser(int userId) { 575 List<Bubble> removedBubbles = filterAllBubbles(bubble -> 576 userId == bubble.getUser().getIdentifier()); 577 for (Bubble b : removedBubbles) { 578 doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED); 579 } 580 if (!removedBubbles.isEmpty()) { 581 dispatchPendingChanges(); 582 } 583 } 584 doAdd(Bubble bubble)585 private void doAdd(Bubble bubble) { 586 if (DEBUG_BUBBLE_DATA) { 587 Log.d(TAG, "doAdd: " + bubble); 588 } 589 mBubbles.add(0, bubble); 590 mStateChange.addedBubble = bubble; 591 // Adding the first bubble doesn't change the order 592 mStateChange.orderChanged = mBubbles.size() > 1; 593 if (!isExpanded()) { 594 setSelectedBubbleInternal(mBubbles.get(0)); 595 } 596 } 597 trim()598 private void trim() { 599 if (mBubbles.size() > mMaxBubbles) { 600 int numtoRemove = mBubbles.size() - mMaxBubbles; 601 ArrayList<Bubble> toRemove = new ArrayList<>(); 602 mBubbles.stream() 603 // sort oldest first (ascending lastActivity) 604 .sorted(Comparator.comparingLong(Bubble::getLastActivity)) 605 // skip the selected bubble 606 .filter((b) -> !b.equals(mSelectedBubble)) 607 .forEachOrdered((b) -> { 608 if (toRemove.size() < numtoRemove) { 609 toRemove.add(b); 610 } 611 }); 612 toRemove.forEach((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED)); 613 } 614 } 615 doUpdate(Bubble bubble, boolean reorder)616 private void doUpdate(Bubble bubble, boolean reorder) { 617 if (DEBUG_BUBBLE_DATA) { 618 Log.d(TAG, "doUpdate: " + bubble); 619 } 620 mStateChange.updatedBubble = bubble; 621 if (!isExpanded() && reorder) { 622 int prevPos = mBubbles.indexOf(bubble); 623 mBubbles.remove(bubble); 624 mBubbles.add(0, bubble); 625 mStateChange.orderChanged = prevPos != 0; 626 setSelectedBubbleInternal(mBubbles.get(0)); 627 } 628 } 629 630 /** Runs the given action on Bubbles that match the given predicate. */ performActionOnBubblesMatching( List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action)631 private void performActionOnBubblesMatching( 632 List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) { 633 final List<Bubble> matchingBubbles = new ArrayList<>(); 634 for (Bubble bubble : bubbles) { 635 if (predicate.test(bubble)) { 636 matchingBubbles.add(bubble); 637 } 638 } 639 640 for (Bubble matchingBubble : matchingBubbles) { 641 action.accept(matchingBubble); 642 } 643 } 644 doRemove(String key, @DismissReason int reason)645 private void doRemove(String key, @DismissReason int reason) { 646 if (DEBUG_BUBBLE_DATA) { 647 Log.d(TAG, "doRemove: " + key); 648 } 649 // If it was pending remove it 650 if (mPendingBubbles.containsKey(key)) { 651 mPendingBubbles.remove(key); 652 } 653 654 boolean shouldRemoveHiddenBubble = reason == Bubbles.DISMISS_NOTIF_CANCEL 655 || reason == Bubbles.DISMISS_GROUP_CANCELLED 656 || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE 657 || reason == Bubbles.DISMISS_BLOCKED 658 || reason == Bubbles.DISMISS_SHORTCUT_REMOVED 659 || reason == Bubbles.DISMISS_PACKAGE_REMOVED 660 || reason == Bubbles.DISMISS_USER_CHANGED 661 || reason == Bubbles.DISMISS_USER_REMOVED; 662 663 int indexToRemove = indexForKey(key); 664 if (indexToRemove == -1) { 665 if (hasOverflowBubbleWithKey(key) 666 && shouldRemoveHiddenBubble) { 667 668 Bubble b = getOverflowBubbleWithKey(key); 669 if (DEBUG_BUBBLE_DATA) { 670 Log.d(TAG, "Cancel overflow bubble: " + b); 671 } 672 if (b != null) { 673 b.stopInflation(); 674 } 675 mLogger.logOverflowRemove(b, reason); 676 mOverflowBubbles.remove(b); 677 mStateChange.bubbleRemoved(b, reason); 678 mStateChange.removedOverflowBubble = b; 679 } 680 if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) { 681 Bubble b = getSuppressedBubbleWithKey(key); 682 if (DEBUG_BUBBLE_DATA) { 683 Log.d(TAG, "Cancel suppressed bubble: " + b); 684 } 685 if (b != null) { 686 mSuppressedBubbles.remove(b.getLocusId()); 687 b.stopInflation(); 688 mStateChange.bubbleRemoved(b, reason); 689 } 690 } 691 return; 692 } 693 Bubble bubbleToRemove = mBubbles.get(indexToRemove); 694 bubbleToRemove.stopInflation(); 695 overflowBubble(reason, bubbleToRemove); 696 697 if (mBubbles.size() == 1) { 698 setExpandedInternal(false); 699 // Don't use setSelectedBubbleInternal because we don't want to trigger an 700 // applyUpdate 701 mSelectedBubble = null; 702 } 703 if (indexToRemove < mBubbles.size() - 1) { 704 // Removing anything but the last bubble means positions will change. 705 mStateChange.orderChanged = true; 706 } 707 mBubbles.remove(indexToRemove); 708 mStateChange.bubbleRemoved(bubbleToRemove, reason); 709 if (!isExpanded()) { 710 mStateChange.orderChanged |= repackAll(); 711 } 712 713 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. 714 if (Objects.equals(mSelectedBubble, bubbleToRemove)) { 715 setNewSelectedIndex(indexToRemove); 716 } 717 maybeSendDeleteIntent(reason, bubbleToRemove); 718 } 719 setNewSelectedIndex(int indexOfSelected)720 private void setNewSelectedIndex(int indexOfSelected) { 721 if (mBubbles.isEmpty()) { 722 Log.w(TAG, "Bubbles list empty when attempting to select index: " + indexOfSelected); 723 return; 724 } 725 // Move selection to the new bubble at the same position. 726 int newIndex = Math.min(indexOfSelected, mBubbles.size() - 1); 727 if (DEBUG_BUBBLE_DATA) { 728 Log.d(TAG, "setNewSelectedIndex: " + indexOfSelected); 729 } 730 BubbleViewProvider newSelected = mBubbles.get(newIndex); 731 setSelectedBubbleInternal(newSelected); 732 } 733 doSuppress(Bubble bubble)734 private void doSuppress(Bubble bubble) { 735 if (DEBUG_BUBBLE_DATA) { 736 Log.d(TAG, "doSuppressed: " + bubble); 737 } 738 mStateChange.suppressedBubble = bubble; 739 bubble.setSuppressBubble(true); 740 741 int indexToRemove = mBubbles.indexOf(bubble); 742 // Order changes if we are not suppressing the last bubble 743 mStateChange.orderChanged = !(mBubbles.size() - 1 == indexToRemove); 744 mBubbles.remove(indexToRemove); 745 746 // Update selection if we suppressed the selected bubble 747 if (Objects.equals(mSelectedBubble, bubble)) { 748 if (mBubbles.isEmpty()) { 749 // Don't use setSelectedBubbleInternal because we don't want to trigger an 750 // applyUpdate 751 mSelectedBubble = null; 752 } else { 753 // Mark new first bubble as selected 754 setNewSelectedIndex(0); 755 } 756 } 757 } 758 doUnsuppress(Bubble bubble)759 private void doUnsuppress(Bubble bubble) { 760 if (DEBUG_BUBBLE_DATA) { 761 Log.d(TAG, "doUnsuppressed: " + bubble); 762 } 763 bubble.setSuppressBubble(false); 764 mStateChange.unsuppressedBubble = bubble; 765 mBubbles.add(bubble); 766 if (mBubbles.size() > 1) { 767 // See where the bubble actually lands 768 repackAll(); 769 mStateChange.orderChanged = true; 770 } 771 if (mBubbles.get(0) == bubble) { 772 // Unsuppressed bubble is sorted to first position. Mark it as the selected. 773 setNewSelectedIndex(0); 774 } 775 } 776 overflowBubble(@ismissReason int reason, Bubble bubble)777 void overflowBubble(@DismissReason int reason, Bubble bubble) { 778 if (bubble.getPendingIntentCanceled() 779 || !(reason == Bubbles.DISMISS_AGED 780 || reason == Bubbles.DISMISS_USER_GESTURE 781 || reason == Bubbles.DISMISS_RELOAD_FROM_DISK) 782 || bubble.isAppBubble()) { 783 return; 784 } 785 if (DEBUG_BUBBLE_DATA) { 786 Log.d(TAG, "Overflowing: " + bubble); 787 } 788 mLogger.logOverflowAdd(bubble, reason); 789 mOverflowBubbles.remove(bubble); 790 mOverflowBubbles.add(0, bubble); 791 mStateChange.addedOverflowBubble = bubble; 792 bubble.stopInflation(); 793 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { 794 // Remove oldest bubble. 795 Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1); 796 if (DEBUG_BUBBLE_DATA) { 797 Log.d(TAG, "Overflow full. Remove: " + oldest); 798 } 799 mStateChange.bubbleRemoved(oldest, Bubbles.DISMISS_OVERFLOW_MAX_REACHED); 800 mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED); 801 mOverflowBubbles.remove(oldest); 802 mStateChange.removedOverflowBubble = oldest; 803 } 804 } 805 dismissAll(@ismissReason int reason)806 public void dismissAll(@DismissReason int reason) { 807 if (DEBUG_BUBBLE_DATA) { 808 Log.d(TAG, "dismissAll: reason=" + reason); 809 } 810 if (mBubbles.isEmpty() && mSuppressedBubbles.isEmpty()) { 811 return; 812 } 813 setExpandedInternal(false); 814 setSelectedBubbleInternal(null); 815 while (!mBubbles.isEmpty()) { 816 doRemove(mBubbles.get(0).getKey(), reason); 817 } 818 while (!mSuppressedBubbles.isEmpty()) { 819 Bubble bubble = mSuppressedBubbles.removeAt(0); 820 doRemove(bubble.getKey(), reason); 821 } 822 dispatchPendingChanges(); 823 } 824 825 /** 826 * Called in response to the visibility of a locusId changing. A locusId is set on a task 827 * and if there's a matching bubble for that locusId then the bubble may be hidden or shown 828 * depending on the visibility of the locusId. 829 * 830 * @param taskId the taskId associated with the locusId visibility change. 831 * @param locusId the locusId whose visibility has changed. 832 * @param visible whether the task with the locusId is visible or not. 833 */ onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible)834 public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) { 835 if (DEBUG_BUBBLE_DATA) { 836 Log.d(TAG, "onLocusVisibilityChanged: " + locusId + " visible=" + visible); 837 } 838 839 Bubble matchingBubble = getBubbleInStackWithLocusId(locusId); 840 // Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled. 841 if (visible && (matchingBubble == null || matchingBubble.getTaskId() != taskId)) { 842 mVisibleLocusIds.add(locusId); 843 } else { 844 mVisibleLocusIds.remove(locusId); 845 } 846 if (matchingBubble == null) { 847 // Check if there is a suppressed bubble for this LocusId 848 matchingBubble = mSuppressedBubbles.get(locusId); 849 if (matchingBubble == null) { 850 return; 851 } 852 } 853 boolean isAlreadySuppressed = mSuppressedBubbles.get(locusId) != null; 854 if (visible && !isAlreadySuppressed && matchingBubble.isSuppressable() 855 && taskId != matchingBubble.getTaskId()) { 856 mSuppressedBubbles.put(locusId, matchingBubble); 857 doSuppress(matchingBubble); 858 dispatchPendingChanges(); 859 } else if (!visible) { 860 Bubble unsuppressedBubble = mSuppressedBubbles.remove(locusId); 861 if (unsuppressedBubble != null) { 862 doUnsuppress(unsuppressedBubble); 863 } 864 dispatchPendingChanges(); 865 } 866 } 867 868 /** 869 * Removes all bubbles from the overflow, called when the user changes. 870 */ clearOverflow()871 public void clearOverflow() { 872 while (!mOverflowBubbles.isEmpty()) { 873 doRemove(mOverflowBubbles.get(0).getKey(), Bubbles.DISMISS_USER_CHANGED); 874 } 875 dispatchPendingChanges(); 876 } 877 dispatchPendingChanges()878 private void dispatchPendingChanges() { 879 if (mListener != null && mStateChange.anythingChanged()) { 880 mListener.applyUpdate(mStateChange); 881 } 882 mStateChange = new Update(mBubbles, mOverflowBubbles); 883 } 884 885 /** 886 * Requests a change to the selected bubble. 887 * 888 * @param bubble the new selected bubble 889 */ setSelectedBubbleInternal(@ullable BubbleViewProvider bubble)890 private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) { 891 if (DEBUG_BUBBLE_DATA) { 892 Log.d(TAG, "setSelectedBubbleInternal: " + bubble); 893 } 894 if (Objects.equals(bubble, mSelectedBubble)) { 895 return; 896 } 897 boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey()); 898 if (bubble != null 899 && !mBubbles.contains(bubble) 900 && !mOverflowBubbles.contains(bubble) 901 && !isOverflow) { 902 Log.e(TAG, "Cannot select bubble which doesn't exist!" 903 + " (" + bubble + ") bubbles=" + mBubbles); 904 return; 905 } 906 if (mExpanded && bubble != null && !isOverflow) { 907 ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); 908 } 909 mSelectedBubble = bubble; 910 mStateChange.selectedBubble = bubble; 911 mStateChange.selectionChanged = true; 912 } 913 setCurrentUserId(int uid)914 void setCurrentUserId(int uid) { 915 mCurrentUserId = uid; 916 } 917 918 /** 919 * Logs the bubble UI event. 920 * 921 * @param provider The bubble view provider that is being interacted on. Null value indicates 922 * that the user interaction is not specific to one bubble. 923 * @param action The user interaction enum 924 * @param packageName SystemUI package 925 * @param bubbleCount Number of bubbles in the stack 926 * @param bubbleIndex Index of bubble in the stack 927 * @param normalX Normalized x position of the stack 928 * @param normalY Normalized y position of the stack 929 */ logBubbleEvent(@ullable BubbleViewProvider provider, int action, String packageName, int bubbleCount, int bubbleIndex, float normalX, float normalY)930 void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, 931 int bubbleCount, int bubbleIndex, float normalX, float normalY) { 932 if (provider == null) { 933 mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY); 934 } else if (provider.getKey().equals(BubbleOverflow.KEY)) { 935 if (action == FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) { 936 mLogger.logShowOverflow(packageName, mCurrentUserId); 937 } 938 } else { 939 mLogger.logBubbleUiChanged((Bubble) provider, packageName, action, bubbleCount, normalX, 940 normalY, bubbleIndex); 941 } 942 } 943 944 /** 945 * Requests a change to the expanded state. 946 * 947 * @param shouldExpand the new requested state 948 */ setExpandedInternal(boolean shouldExpand)949 private void setExpandedInternal(boolean shouldExpand) { 950 if (DEBUG_BUBBLE_DATA) { 951 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); 952 } 953 if (mExpanded == shouldExpand) { 954 return; 955 } 956 if (shouldExpand) { 957 if (mBubbles.isEmpty() && !mShowingOverflow) { 958 Log.e(TAG, "Attempt to expand stack when empty!"); 959 return; 960 } 961 if (mSelectedBubble == null) { 962 Log.e(TAG, "Attempt to expand stack without selected bubble!"); 963 return; 964 } 965 if (mSelectedBubble.getKey().equals(mOverflow.getKey()) && !mBubbles.isEmpty()) { 966 // Show previously selected bubble instead of overflow menu when expanding. 967 setSelectedBubbleInternal(mBubbles.get(0)); 968 } 969 if (mSelectedBubble instanceof Bubble) { 970 ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); 971 } 972 mStateChange.orderChanged |= repackAll(); 973 } else if (!mBubbles.isEmpty()) { 974 // Apply ordering and grouping rules from expanded -> collapsed, then save 975 // the result. 976 mStateChange.orderChanged |= repackAll(); 977 if (mBubbles.indexOf(mSelectedBubble) > 0) { 978 // Move the selected bubble to the top while collapsed. 979 int index = mBubbles.indexOf(mSelectedBubble); 980 if (index != 0) { 981 mBubbles.remove((Bubble) mSelectedBubble); 982 mBubbles.add(0, (Bubble) mSelectedBubble); 983 mStateChange.orderChanged = true; 984 } 985 } 986 } 987 if (mNeedsTrimming) { 988 mNeedsTrimming = false; 989 trim(); 990 } 991 mExpanded = shouldExpand; 992 mStateChange.expanded = shouldExpand; 993 mStateChange.expandedChanged = true; 994 } 995 sortKey(Bubble bubble)996 private static long sortKey(Bubble bubble) { 997 return bubble.getLastActivity(); 998 } 999 1000 /** 1001 * This applies a full sort and group pass to all existing bubbles. 1002 * Bubbles are sorted by lastUpdated descending. 1003 * 1004 * @return true if the position of any bubbles changed as a result 1005 */ repackAll()1006 private boolean repackAll() { 1007 if (DEBUG_BUBBLE_DATA) { 1008 Log.d(TAG, "repackAll()"); 1009 } 1010 if (mBubbles.isEmpty()) { 1011 return false; 1012 } 1013 List<Bubble> repacked = new ArrayList<>(mBubbles.size()); 1014 // Add bubbles, freshest to oldest 1015 mBubbles.stream() 1016 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) 1017 .forEachOrdered(repacked::add); 1018 if (repacked.equals(mBubbles)) { 1019 return false; 1020 } 1021 mBubbles.clear(); 1022 mBubbles.addAll(repacked); 1023 return true; 1024 } 1025 maybeSendDeleteIntent(@ismissReason int reason, @NonNull final Bubble bubble)1026 private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) { 1027 if (reason != Bubbles.DISMISS_USER_GESTURE) return; 1028 PendingIntent deleteIntent = bubble.getDeleteIntent(); 1029 if (deleteIntent == null) return; 1030 try { 1031 deleteIntent.send(); 1032 } catch (PendingIntent.CanceledException e) { 1033 Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey()); 1034 } 1035 } 1036 indexForKey(String key)1037 private int indexForKey(String key) { 1038 for (int i = 0; i < mBubbles.size(); i++) { 1039 Bubble bubble = mBubbles.get(i); 1040 if (bubble.getKey().equals(key)) { 1041 return i; 1042 } 1043 } 1044 return -1; 1045 } 1046 1047 /** 1048 * The set of bubbles in row. 1049 */ 1050 @VisibleForTesting(visibility = PACKAGE) getBubbles()1051 public List<Bubble> getBubbles() { 1052 return Collections.unmodifiableList(mBubbles); 1053 } 1054 1055 /** 1056 * The set of bubbles in overflow. 1057 */ 1058 @VisibleForTesting(visibility = PRIVATE) getOverflowBubbles()1059 public List<Bubble> getOverflowBubbles() { 1060 return Collections.unmodifiableList(mOverflowBubbles); 1061 } 1062 1063 @VisibleForTesting(visibility = PRIVATE) 1064 @Nullable getAnyBubbleWithkey(String key)1065 Bubble getAnyBubbleWithkey(String key) { 1066 Bubble b = getBubbleInStackWithKey(key); 1067 if (b == null) { 1068 b = getOverflowBubbleWithKey(key); 1069 } 1070 if (b == null) { 1071 b = getSuppressedBubbleWithKey(key); 1072 } 1073 return b; 1074 } 1075 1076 /** @return any bubble (in the stack or the overflow) that matches the provided shortcutId. */ 1077 @Nullable getAnyBubbleWithShortcutId(String shortcutId)1078 Bubble getAnyBubbleWithShortcutId(String shortcutId) { 1079 if (TextUtils.isEmpty(shortcutId)) { 1080 return null; 1081 } 1082 for (int i = 0; i < mBubbles.size(); i++) { 1083 Bubble bubble = mBubbles.get(i); 1084 String bubbleShortcutId = bubble.getShortcutInfo() != null 1085 ? bubble.getShortcutInfo().getId() 1086 : bubble.getMetadataShortcutId(); 1087 if (shortcutId.equals(bubbleShortcutId)) { 1088 return bubble; 1089 } 1090 } 1091 1092 for (int i = 0; i < mOverflowBubbles.size(); i++) { 1093 Bubble bubble = mOverflowBubbles.get(i); 1094 String bubbleShortcutId = bubble.getShortcutInfo() != null 1095 ? bubble.getShortcutInfo().getId() 1096 : bubble.getMetadataShortcutId(); 1097 if (shortcutId.equals(bubbleShortcutId)) { 1098 return bubble; 1099 } 1100 } 1101 return null; 1102 } 1103 1104 @VisibleForTesting(visibility = PRIVATE) 1105 @Nullable getBubbleInStackWithKey(String key)1106 public Bubble getBubbleInStackWithKey(String key) { 1107 for (int i = 0; i < mBubbles.size(); i++) { 1108 Bubble bubble = mBubbles.get(i); 1109 if (bubble.getKey().equals(key)) { 1110 return bubble; 1111 } 1112 } 1113 return null; 1114 } 1115 1116 @Nullable getBubbleInStackWithLocusId(LocusId locusId)1117 private Bubble getBubbleInStackWithLocusId(LocusId locusId) { 1118 if (locusId == null) return null; 1119 for (int i = 0; i < mBubbles.size(); i++) { 1120 Bubble bubble = mBubbles.get(i); 1121 if (locusId.equals(bubble.getLocusId())) { 1122 return bubble; 1123 } 1124 } 1125 return null; 1126 } 1127 1128 @Nullable getBubbleWithView(View view)1129 Bubble getBubbleWithView(View view) { 1130 for (int i = 0; i < mBubbles.size(); i++) { 1131 Bubble bubble = mBubbles.get(i); 1132 if (bubble.getIconView() != null && bubble.getIconView().equals(view)) { 1133 return bubble; 1134 } 1135 } 1136 return null; 1137 } 1138 1139 @VisibleForTesting(visibility = PRIVATE) getOverflowBubbleWithKey(String key)1140 public Bubble getOverflowBubbleWithKey(String key) { 1141 for (int i = 0; i < mOverflowBubbles.size(); i++) { 1142 Bubble bubble = mOverflowBubbles.get(i); 1143 if (bubble.getKey().equals(key)) { 1144 return bubble; 1145 } 1146 } 1147 return null; 1148 } 1149 1150 /** 1151 * Get a suppressed bubble with given notification <code>key</code> 1152 * 1153 * @param key notification key 1154 * @return bubble that matches or null 1155 */ 1156 @Nullable 1157 @VisibleForTesting(visibility = PRIVATE) getSuppressedBubbleWithKey(String key)1158 public Bubble getSuppressedBubbleWithKey(String key) { 1159 for (Bubble b : mSuppressedBubbles.values()) { 1160 if (b.getKey().equals(key)) { 1161 return b; 1162 } 1163 } 1164 return null; 1165 } 1166 1167 /** 1168 * Get a pending bubble with given notification <code>key</code> 1169 * 1170 * @param key notification key 1171 * @return bubble that matches or null 1172 */ 1173 @VisibleForTesting(visibility = PRIVATE) getPendingBubbleWithKey(String key)1174 public Bubble getPendingBubbleWithKey(String key) { 1175 for (Bubble b : mPendingBubbles.values()) { 1176 if (b.getKey().equals(key)) { 1177 return b; 1178 } 1179 } 1180 return null; 1181 } 1182 1183 /** 1184 * Returns a list of bubbles that match the provided predicate. This checks all types of 1185 * bubbles (i.e. pending, suppressed, active, and overflowed). 1186 */ filterAllBubbles(Predicate<Bubble> predicate)1187 private List<Bubble> filterAllBubbles(Predicate<Bubble> predicate) { 1188 ArrayList<Bubble> matchingBubbles = new ArrayList<>(); 1189 for (Bubble b : mPendingBubbles.values()) { 1190 if (predicate.test(b)) { 1191 matchingBubbles.add(b); 1192 } 1193 } 1194 for (Bubble b : mSuppressedBubbles.values()) { 1195 if (predicate.test(b)) { 1196 matchingBubbles.add(b); 1197 } 1198 } 1199 for (Bubble b : mBubbles) { 1200 if (predicate.test(b)) { 1201 matchingBubbles.add(b); 1202 } 1203 } 1204 for (Bubble b : mOverflowBubbles) { 1205 if (predicate.test(b)) { 1206 matchingBubbles.add(b); 1207 } 1208 } 1209 return matchingBubbles; 1210 } 1211 1212 @VisibleForTesting(visibility = PRIVATE) setTimeSource(TimeSource timeSource)1213 void setTimeSource(TimeSource timeSource) { 1214 mTimeSource = timeSource; 1215 } 1216 setListener(Listener listener)1217 public void setListener(Listener listener) { 1218 mListener = listener; 1219 } 1220 1221 /** 1222 * Set maximum number of bubbles allowed in overflow. 1223 * This method should only be used in tests, not in production. 1224 */ 1225 @VisibleForTesting setMaxOverflowBubbles(int maxOverflowBubbles)1226 public void setMaxOverflowBubbles(int maxOverflowBubbles) { 1227 mMaxOverflowBubbles = maxOverflowBubbles; 1228 } 1229 1230 /** 1231 * Description of current bubble data state. 1232 */ dump(PrintWriter pw)1233 public void dump(PrintWriter pw) { 1234 pw.print("selected: "); 1235 pw.println(mSelectedBubble != null 1236 ? mSelectedBubble.getKey() 1237 : "null"); 1238 pw.print("expanded: "); 1239 pw.println(mExpanded); 1240 1241 pw.print("stack bubble count: "); 1242 pw.println(mBubbles.size()); 1243 for (Bubble bubble : mBubbles) { 1244 bubble.dump(pw); 1245 } 1246 1247 pw.print("overflow bubble count: "); 1248 pw.println(mOverflowBubbles.size()); 1249 for (Bubble bubble : mOverflowBubbles) { 1250 bubble.dump(pw); 1251 } 1252 1253 pw.print("summaryKeys: "); 1254 pw.println(mSuppressedGroupKeys.size()); 1255 for (String key : mSuppressedGroupKeys.keySet()) { 1256 pw.println(" suppressing: " + key); 1257 } 1258 } 1259 } 1260