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