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