1 /* 2 * Copyright (C) 2019 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.systemui.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.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; 21 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; 22 import static com.android.systemui.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.pm.ShortcutInfo; 28 import android.util.Log; 29 import android.util.Pair; 30 import android.view.View; 31 32 import androidx.annotation.Nullable; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.systemui.R; 36 import com.android.systemui.bubbles.BubbleController.DismissReason; 37 import com.android.systemui.statusbar.notification.NotificationEntryManager; 38 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 39 40 import java.io.FileDescriptor; 41 import java.io.PrintWriter; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.Comparator; 45 import java.util.HashMap; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.Set; 50 import java.util.function.Consumer; 51 import java.util.function.Predicate; 52 53 import javax.inject.Inject; 54 import javax.inject.Singleton; 55 56 /** 57 * Keeps track of active bubbles. 58 */ 59 @Singleton 60 public class BubbleData { 61 62 private BubbleLogger mLogger = new BubbleLoggerImpl(); 63 64 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES; 65 66 private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = 67 Comparator.comparing(BubbleData::sortKey).reversed(); 68 69 /** Contains information about changes that have been made to the state of bubbles. */ 70 static final class Update { 71 boolean expandedChanged; 72 boolean selectionChanged; 73 boolean orderChanged; 74 boolean expanded; 75 @Nullable Bubble selectedBubble; 76 @Nullable Bubble addedBubble; 77 @Nullable Bubble updatedBubble; 78 // Pair with Bubble and @DismissReason Integer 79 final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); 80 81 // A read-only view of the bubbles list, changes there will be reflected here. 82 final List<Bubble> bubbles; 83 final List<Bubble> overflowBubbles; 84 Update(List<Bubble> row, List<Bubble> overflow)85 private Update(List<Bubble> row, List<Bubble> overflow) { 86 bubbles = Collections.unmodifiableList(row); 87 overflowBubbles = Collections.unmodifiableList(overflow); 88 } 89 anythingChanged()90 boolean anythingChanged() { 91 return expandedChanged 92 || selectionChanged 93 || addedBubble != null 94 || updatedBubble != null 95 || !removedBubbles.isEmpty() 96 || orderChanged; 97 } 98 bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason)99 void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { 100 removedBubbles.add(new Pair<>(bubbleToRemove, reason)); 101 } 102 } 103 104 /** 105 * This interface reports changes to the state and appearance of bubbles which should be applied 106 * as necessary to the UI. 107 */ 108 interface Listener { 109 /** Reports changes have have occurred as a result of the most recent operation. */ applyUpdate(Update update)110 void applyUpdate(Update update); 111 } 112 113 interface TimeSource { currentTimeMillis()114 long currentTimeMillis(); 115 } 116 117 private final Context mContext; 118 /** Bubbles that are actively in the stack. */ 119 private final List<Bubble> mBubbles; 120 /** Bubbles that aged out to overflow. */ 121 private final List<Bubble> mOverflowBubbles; 122 /** Bubbles that are being loaded but haven't been added to the stack just yet. */ 123 private final HashMap<String, Bubble> mPendingBubbles; 124 private Bubble mSelectedBubble; 125 private boolean mShowingOverflow; 126 private boolean mExpanded; 127 private final int mMaxBubbles; 128 private int mMaxOverflowBubbles; 129 130 // State tracked during an operation -- keeps track of what listener events to dispatch. 131 private Update mStateChange; 132 133 private TimeSource mTimeSource = System::currentTimeMillis; 134 135 @Nullable 136 private Listener mListener; 137 138 @Nullable 139 private BubbleController.NotificationSuppressionChangedListener mSuppressionListener; 140 private BubbleController.PendingIntentCanceledListener mCancelledListener; 141 142 /** 143 * We track groups with summaries that aren't visibly displayed but still kept around because 144 * the bubble(s) associated with the summary still exist. 145 * 146 * The summary must be kept around so that developers can cancel it (and hence the bubbles 147 * associated with it). This list is used to check if the summary should be hidden from the 148 * shade. 149 * 150 * Key: group key of the NotificationEntry 151 * Value: key of the NotificationEntry 152 */ 153 private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); 154 155 @Inject BubbleData(Context context)156 public BubbleData(Context context) { 157 mContext = context; 158 mBubbles = new ArrayList<>(); 159 mOverflowBubbles = new ArrayList<>(); 160 mPendingBubbles = new HashMap<>(); 161 mStateChange = new Update(mBubbles, mOverflowBubbles); 162 mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered); 163 mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); 164 } 165 setSuppressionChangedListener( BubbleController.NotificationSuppressionChangedListener listener)166 public void setSuppressionChangedListener( 167 BubbleController.NotificationSuppressionChangedListener listener) { 168 mSuppressionListener = listener; 169 } 170 setPendingIntentCancelledListener( BubbleController.PendingIntentCanceledListener listener)171 public void setPendingIntentCancelledListener( 172 BubbleController.PendingIntentCanceledListener listener) { 173 mCancelledListener = listener; 174 } 175 hasBubbles()176 public boolean hasBubbles() { 177 return !mBubbles.isEmpty(); 178 } 179 isExpanded()180 public boolean isExpanded() { 181 return mExpanded; 182 } 183 hasAnyBubbleWithKey(String key)184 public boolean hasAnyBubbleWithKey(String key) { 185 return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key); 186 } 187 hasBubbleInStackWithKey(String key)188 public boolean hasBubbleInStackWithKey(String key) { 189 return getBubbleInStackWithKey(key) != null; 190 } 191 hasOverflowBubbleWithKey(String key)192 public boolean hasOverflowBubbleWithKey(String key) { 193 return getOverflowBubbleWithKey(key) != null; 194 } 195 196 @Nullable getSelectedBubble()197 public Bubble getSelectedBubble() { 198 return mSelectedBubble; 199 } 200 setExpanded(boolean expanded)201 public void setExpanded(boolean expanded) { 202 if (DEBUG_BUBBLE_DATA) { 203 Log.d(TAG, "setExpanded: " + expanded); 204 } 205 setExpandedInternal(expanded); 206 dispatchPendingChanges(); 207 } 208 setSelectedBubble(Bubble bubble)209 public void setSelectedBubble(Bubble bubble) { 210 if (DEBUG_BUBBLE_DATA) { 211 Log.d(TAG, "setSelectedBubble: " + bubble); 212 } 213 setSelectedBubbleInternal(bubble); 214 dispatchPendingChanges(); 215 } 216 setShowingOverflow(boolean showingOverflow)217 void setShowingOverflow(boolean showingOverflow) { 218 mShowingOverflow = showingOverflow; 219 } 220 221 /** 222 * Constructs a new bubble or returns an existing one. Does not add new bubbles to 223 * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)} 224 * for that. 225 * 226 * @param entry The notification entry to use, only null if it's a bubble being promoted from 227 * the overflow that was persisted over reboot. 228 * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from 229 * the overflow that was persisted over reboot. 230 */ getOrCreateBubble(NotificationEntry entry, Bubble persistedBubble)231 Bubble getOrCreateBubble(NotificationEntry entry, Bubble persistedBubble) { 232 String key = entry != null ? entry.getKey() : persistedBubble.getKey(); 233 Bubble bubbleToReturn = getBubbleInStackWithKey(key); 234 235 if (bubbleToReturn == null) { 236 bubbleToReturn = getOverflowBubbleWithKey(key); 237 if (bubbleToReturn != null) { 238 // Promoting from overflow 239 mOverflowBubbles.remove(bubbleToReturn); 240 } else if (mPendingBubbles.containsKey(key)) { 241 // Update while it was pending 242 bubbleToReturn = mPendingBubbles.get(key); 243 } else if (entry != null) { 244 // New bubble 245 bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener); 246 } else { 247 // Persisted bubble being promoted 248 bubbleToReturn = persistedBubble; 249 } 250 } 251 252 if (entry != null) { 253 bubbleToReturn.setEntry(entry); 254 } 255 mPendingBubbles.put(key, bubbleToReturn); 256 return bubbleToReturn; 257 } 258 259 /** 260 * When this method is called it is expected that all info in the bubble has completed loading. 261 * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, 262 * BubbleStackView, BubbleIconFactory). 263 */ notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade)264 void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { 265 if (DEBUG_BUBBLE_DATA) { 266 Log.d(TAG, "notificationEntryUpdated: " + bubble); 267 } 268 mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here 269 Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); 270 suppressFlyout |= !bubble.isVisuallyInterruptive(); 271 272 if (prevBubble == null) { 273 // Create a new bubble 274 bubble.setSuppressFlyout(suppressFlyout); 275 doAdd(bubble); 276 trim(); 277 } else { 278 // Updates an existing bubble 279 bubble.setSuppressFlyout(suppressFlyout); 280 // If there is no flyout, we probably shouldn't show the bubble at the top 281 doUpdate(bubble, !suppressFlyout /* reorder */); 282 } 283 284 if (bubble.shouldAutoExpand()) { 285 bubble.setShouldAutoExpand(false); 286 setSelectedBubbleInternal(bubble); 287 if (!mExpanded) { 288 setExpandedInternal(true); 289 } 290 } 291 292 boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble; 293 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade(); 294 bubble.setSuppressNotification(suppress); 295 bubble.setShowDot(!isBubbleExpandedAndSelected /* show */); 296 297 dispatchPendingChanges(); 298 } 299 300 /** 301 * Dismisses the bubble with the matching key, if it exists. 302 */ dismissBubbleWithKey(String key, @DismissReason int reason)303 public void dismissBubbleWithKey(String key, @DismissReason int reason) { 304 if (DEBUG_BUBBLE_DATA) { 305 Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason); 306 } 307 doRemove(key, reason); 308 dispatchPendingChanges(); 309 } 310 311 /** 312 * Adds a group key indicating that the summary for this group should be suppressed. 313 * 314 * @param groupKey the group key of the group whose summary should be suppressed. 315 * @param notifKey the notification entry key of that summary. 316 */ addSummaryToSuppress(String groupKey, String notifKey)317 void addSummaryToSuppress(String groupKey, String notifKey) { 318 mSuppressedGroupKeys.put(groupKey, notifKey); 319 } 320 321 /** 322 * Retrieves the notif entry key of the summary associated with the provided group key. 323 * 324 * @param groupKey the group to look up 325 * @return the key for the {@link NotificationEntry} that is the summary of this group. 326 */ getSummaryKey(String groupKey)327 String getSummaryKey(String groupKey) { 328 return mSuppressedGroupKeys.get(groupKey); 329 } 330 331 /** 332 * Removes a group key indicating that summary for this group should no longer be suppressed. 333 */ removeSuppressedSummary(String groupKey)334 void removeSuppressedSummary(String groupKey) { 335 mSuppressedGroupKeys.remove(groupKey); 336 } 337 338 /** 339 * Whether the summary for the provided group key is suppressed. 340 */ isSummarySuppressed(String groupKey)341 boolean isSummarySuppressed(String groupKey) { 342 return mSuppressedGroupKeys.containsKey(groupKey); 343 } 344 345 /** 346 * Retrieves any bubbles that are part of the notification group represented by the provided 347 * group key. 348 */ getBubblesInGroup(@ullable String groupKey, @NonNull NotificationEntryManager nem)349 ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey, @NonNull 350 NotificationEntryManager nem) { 351 ArrayList<Bubble> bubbleChildren = new ArrayList<>(); 352 if (groupKey == null) { 353 return bubbleChildren; 354 } 355 for (Bubble b : mBubbles) { 356 final NotificationEntry entry = nem.getPendingOrActiveNotif(b.getKey()); 357 if (entry != null && groupKey.equals(entry.getSbn().getGroupKey())) { 358 bubbleChildren.add(b); 359 } 360 } 361 return bubbleChildren; 362 } 363 364 /** 365 * Removes bubbles from the given package whose shortcut are not in the provided list of valid 366 * shortcuts. 367 */ removeBubblesWithInvalidShortcuts( String packageName, List<ShortcutInfo> validShortcuts, int reason)368 public void removeBubblesWithInvalidShortcuts( 369 String packageName, List<ShortcutInfo> validShortcuts, int reason) { 370 371 final Set<String> validShortcutIds = new HashSet<String>(); 372 for (ShortcutInfo info : validShortcuts) { 373 validShortcutIds.add(info.getId()); 374 } 375 376 final Predicate<Bubble> invalidBubblesFromPackage = bubble -> { 377 final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName()); 378 final boolean isShortcutBubble = bubble.hasMetadataShortcutId(); 379 if (!bubbleIsFromPackage || !isShortcutBubble) { 380 return false; 381 } 382 final boolean hasShortcutIdAndValidShortcut = 383 bubble.hasMetadataShortcutId() 384 && bubble.getShortcutInfo() != null 385 && bubble.getShortcutInfo().isEnabled() 386 && validShortcutIds.contains(bubble.getShortcutInfo().getId()); 387 return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut; 388 }; 389 390 final Consumer<Bubble> removeBubble = bubble -> 391 dismissBubbleWithKey(bubble.getKey(), reason); 392 393 performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble); 394 performActionOnBubblesMatching( 395 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble); 396 } 397 398 /** Dismisses all bubbles from the given package. */ removeBubblesWithPackageName(String packageName, int reason)399 public void removeBubblesWithPackageName(String packageName, int reason) { 400 final Predicate<Bubble> bubbleMatchesPackage = bubble -> 401 bubble.getPackageName().equals(packageName); 402 403 final Consumer<Bubble> removeBubble = bubble -> 404 dismissBubbleWithKey(bubble.getKey(), reason); 405 406 performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble); 407 performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble); 408 } 409 doAdd(Bubble bubble)410 private void doAdd(Bubble bubble) { 411 if (DEBUG_BUBBLE_DATA) { 412 Log.d(TAG, "doAdd: " + bubble); 413 } 414 mBubbles.add(0, bubble); 415 mStateChange.addedBubble = bubble; 416 // Adding the first bubble doesn't change the order 417 mStateChange.orderChanged = mBubbles.size() > 1; 418 if (!isExpanded()) { 419 setSelectedBubbleInternal(mBubbles.get(0)); 420 } 421 } 422 trim()423 private void trim() { 424 if (mBubbles.size() > mMaxBubbles) { 425 mBubbles.stream() 426 // sort oldest first (ascending lastActivity) 427 .sorted(Comparator.comparingLong(Bubble::getLastActivity)) 428 // skip the selected bubble 429 .filter((b) -> !b.equals(mSelectedBubble)) 430 .findFirst() 431 .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED)); 432 } 433 } 434 doUpdate(Bubble bubble, boolean reorder)435 private void doUpdate(Bubble bubble, boolean reorder) { 436 if (DEBUG_BUBBLE_DATA) { 437 Log.d(TAG, "doUpdate: " + bubble); 438 } 439 mStateChange.updatedBubble = bubble; 440 if (!isExpanded() && reorder) { 441 int prevPos = mBubbles.indexOf(bubble); 442 mBubbles.remove(bubble); 443 mBubbles.add(0, bubble); 444 mStateChange.orderChanged = prevPos != 0; 445 setSelectedBubbleInternal(mBubbles.get(0)); 446 } 447 } 448 449 /** Runs the given action on Bubbles that match the given predicate. */ performActionOnBubblesMatching( List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action)450 private void performActionOnBubblesMatching( 451 List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) { 452 final List<Bubble> matchingBubbles = new ArrayList<>(); 453 for (Bubble bubble : bubbles) { 454 if (predicate.test(bubble)) { 455 matchingBubbles.add(bubble); 456 } 457 } 458 459 for (Bubble matchingBubble : matchingBubbles) { 460 action.accept(matchingBubble); 461 } 462 } 463 doRemove(String key, @DismissReason int reason)464 private void doRemove(String key, @DismissReason int reason) { 465 if (DEBUG_BUBBLE_DATA) { 466 Log.d(TAG, "doRemove: " + key); 467 } 468 // If it was pending remove it 469 if (mPendingBubbles.containsKey(key)) { 470 mPendingBubbles.remove(key); 471 } 472 int indexToRemove = indexForKey(key); 473 if (indexToRemove == -1) { 474 if (hasOverflowBubbleWithKey(key) 475 && (reason == BubbleController.DISMISS_NOTIF_CANCEL 476 || reason == BubbleController.DISMISS_GROUP_CANCELLED 477 || reason == BubbleController.DISMISS_NO_LONGER_BUBBLE 478 || reason == BubbleController.DISMISS_BLOCKED 479 || reason == BubbleController.DISMISS_SHORTCUT_REMOVED 480 || reason == BubbleController.DISMISS_PACKAGE_REMOVED)) { 481 482 Bubble b = getOverflowBubbleWithKey(key); 483 if (DEBUG_BUBBLE_DATA) { 484 Log.d(TAG, "Cancel overflow bubble: " + b); 485 } 486 if (b != null) { 487 b.stopInflation(); 488 } 489 mLogger.logOverflowRemove(b, reason); 490 mStateChange.bubbleRemoved(b, reason); 491 mOverflowBubbles.remove(b); 492 } 493 return; 494 } 495 Bubble bubbleToRemove = mBubbles.get(indexToRemove); 496 bubbleToRemove.stopInflation(); 497 if (mBubbles.size() == 1) { 498 // Going to become empty, handle specially. 499 setExpandedInternal(false); 500 // Don't use setSelectedBubbleInternal because we don't want to trigger an applyUpdate 501 mSelectedBubble = null; 502 } 503 if (indexToRemove < mBubbles.size() - 1) { 504 // Removing anything but the last bubble means positions will change. 505 mStateChange.orderChanged = true; 506 } 507 mBubbles.remove(indexToRemove); 508 mStateChange.bubbleRemoved(bubbleToRemove, reason); 509 if (!isExpanded()) { 510 mStateChange.orderChanged |= repackAll(); 511 } 512 513 overflowBubble(reason, bubbleToRemove); 514 515 // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. 516 if (Objects.equals(mSelectedBubble, bubbleToRemove)) { 517 // Move selection to the new bubble at the same position. 518 int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); 519 Bubble newSelected = mBubbles.get(newIndex); 520 setSelectedBubbleInternal(newSelected); 521 } 522 maybeSendDeleteIntent(reason, bubbleToRemove); 523 } 524 overflowBubble(@ismissReason int reason, Bubble bubble)525 void overflowBubble(@DismissReason int reason, Bubble bubble) { 526 if (bubble.getPendingIntentCanceled() 527 || !(reason == BubbleController.DISMISS_AGED 528 || reason == BubbleController.DISMISS_USER_GESTURE)) { 529 return; 530 } 531 if (DEBUG_BUBBLE_DATA) { 532 Log.d(TAG, "Overflowing: " + bubble); 533 } 534 mLogger.logOverflowAdd(bubble, reason); 535 mOverflowBubbles.add(0, bubble); 536 bubble.stopInflation(); 537 if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) { 538 // Remove oldest bubble. 539 Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1); 540 if (DEBUG_BUBBLE_DATA) { 541 Log.d(TAG, "Overflow full. Remove: " + oldest); 542 } 543 mStateChange.bubbleRemoved(oldest, BubbleController.DISMISS_OVERFLOW_MAX_REACHED); 544 mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED); 545 mOverflowBubbles.remove(oldest); 546 } 547 } 548 dismissAll(@ismissReason int reason)549 public void dismissAll(@DismissReason int reason) { 550 if (DEBUG_BUBBLE_DATA) { 551 Log.d(TAG, "dismissAll: reason=" + reason); 552 } 553 if (mBubbles.isEmpty()) { 554 return; 555 } 556 setExpandedInternal(false); 557 setSelectedBubbleInternal(null); 558 while (!mBubbles.isEmpty()) { 559 doRemove(mBubbles.get(0).getKey(), reason); 560 } 561 dispatchPendingChanges(); 562 } 563 564 /** 565 * Indicates that the provided display is no longer in use and should be cleaned up. 566 * 567 * @param displayId the id of the display to clean up. 568 */ notifyDisplayEmpty(int displayId)569 void notifyDisplayEmpty(int displayId) { 570 for (Bubble b : mBubbles) { 571 if (b.getDisplayId() == displayId) { 572 if (b.getExpandedView() != null) { 573 b.getExpandedView().notifyDisplayEmpty(); 574 } 575 return; 576 } 577 } 578 } 579 dispatchPendingChanges()580 private void dispatchPendingChanges() { 581 if (mListener != null && mStateChange.anythingChanged()) { 582 mListener.applyUpdate(mStateChange); 583 } 584 mStateChange = new Update(mBubbles, mOverflowBubbles); 585 } 586 587 /** 588 * Requests a change to the selected bubble. 589 * 590 * @param bubble the new selected bubble 591 */ setSelectedBubbleInternal(@ullable Bubble bubble)592 private void setSelectedBubbleInternal(@Nullable Bubble bubble) { 593 if (DEBUG_BUBBLE_DATA) { 594 Log.d(TAG, "setSelectedBubbleInternal: " + bubble); 595 } 596 if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { 597 return; 598 } 599 // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. 600 601 if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) { 602 Log.e(TAG, "Cannot select bubble which doesn't exist!" 603 + " (" + bubble + ") bubbles=" + mBubbles); 604 return; 605 } 606 if (mExpanded && bubble != null) { 607 bubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); 608 } 609 mSelectedBubble = bubble; 610 mStateChange.selectedBubble = bubble; 611 mStateChange.selectionChanged = true; 612 } 613 614 /** 615 * Requests a change to the expanded state. 616 * 617 * @param shouldExpand the new requested state 618 */ setExpandedInternal(boolean shouldExpand)619 private void setExpandedInternal(boolean shouldExpand) { 620 if (DEBUG_BUBBLE_DATA) { 621 Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); 622 } 623 if (mExpanded == shouldExpand) { 624 return; 625 } 626 if (shouldExpand) { 627 if (mBubbles.isEmpty()) { 628 Log.e(TAG, "Attempt to expand stack when empty!"); 629 return; 630 } 631 if (mSelectedBubble == null) { 632 Log.e(TAG, "Attempt to expand stack without selected bubble!"); 633 return; 634 } 635 mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); 636 mStateChange.orderChanged |= repackAll(); 637 } else if (!mBubbles.isEmpty()) { 638 // Apply ordering and grouping rules from expanded -> collapsed, then save 639 // the result. 640 mStateChange.orderChanged |= repackAll(); 641 // Save the state which should be returned to when expanded (with no other changes) 642 643 if (mShowingOverflow) { 644 // Show previously selected bubble instead of overflow menu on next expansion. 645 setSelectedBubbleInternal(mSelectedBubble); 646 } 647 if (mBubbles.indexOf(mSelectedBubble) > 0) { 648 // Move the selected bubble to the top while collapsed. 649 int index = mBubbles.indexOf(mSelectedBubble); 650 if (index != 0) { 651 mBubbles.remove(mSelectedBubble); 652 mBubbles.add(0, mSelectedBubble); 653 mStateChange.orderChanged = true; 654 } 655 } 656 } 657 mExpanded = shouldExpand; 658 mStateChange.expanded = shouldExpand; 659 mStateChange.expandedChanged = true; 660 } 661 sortKey(Bubble bubble)662 private static long sortKey(Bubble bubble) { 663 return bubble.getLastActivity(); 664 } 665 666 /** 667 * This applies a full sort and group pass to all existing bubbles. 668 * Bubbles are sorted by lastUpdated descending. 669 * 670 * @return true if the position of any bubbles changed as a result 671 */ repackAll()672 private boolean repackAll() { 673 if (DEBUG_BUBBLE_DATA) { 674 Log.d(TAG, "repackAll()"); 675 } 676 if (mBubbles.isEmpty()) { 677 return false; 678 } 679 List<Bubble> repacked = new ArrayList<>(mBubbles.size()); 680 // Add bubbles, freshest to oldest 681 mBubbles.stream() 682 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) 683 .forEachOrdered(repacked::add); 684 if (repacked.equals(mBubbles)) { 685 return false; 686 } 687 mBubbles.clear(); 688 mBubbles.addAll(repacked); 689 return true; 690 } 691 maybeSendDeleteIntent(@ismissReason int reason, @NonNull final Bubble bubble)692 private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) { 693 if (reason != BubbleController.DISMISS_USER_GESTURE) return; 694 PendingIntent deleteIntent = bubble.getDeleteIntent(); 695 if (deleteIntent == null) return; 696 try { 697 deleteIntent.send(); 698 } catch (PendingIntent.CanceledException e) { 699 Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey()); 700 } 701 } 702 indexForKey(String key)703 private int indexForKey(String key) { 704 for (int i = 0; i < mBubbles.size(); i++) { 705 Bubble bubble = mBubbles.get(i); 706 if (bubble.getKey().equals(key)) { 707 return i; 708 } 709 } 710 return -1; 711 } 712 713 /** 714 * The set of bubbles in row. 715 */ 716 @VisibleForTesting(visibility = PACKAGE) getBubbles()717 public List<Bubble> getBubbles() { 718 return Collections.unmodifiableList(mBubbles); 719 } 720 721 /** 722 * The set of bubbles in overflow. 723 */ 724 @VisibleForTesting(visibility = PRIVATE) getOverflowBubbles()725 List<Bubble> getOverflowBubbles() { 726 return Collections.unmodifiableList(mOverflowBubbles); 727 } 728 729 @VisibleForTesting(visibility = PRIVATE) 730 @Nullable getAnyBubbleWithkey(String key)731 Bubble getAnyBubbleWithkey(String key) { 732 Bubble b = getBubbleInStackWithKey(key); 733 if (b == null) { 734 b = getOverflowBubbleWithKey(key); 735 } 736 return b; 737 } 738 739 @VisibleForTesting(visibility = PRIVATE) 740 @Nullable getBubbleInStackWithKey(String key)741 Bubble getBubbleInStackWithKey(String key) { 742 for (int i = 0; i < mBubbles.size(); i++) { 743 Bubble bubble = mBubbles.get(i); 744 if (bubble.getKey().equals(key)) { 745 return bubble; 746 } 747 } 748 return null; 749 } 750 751 @Nullable getBubbleWithView(View view)752 Bubble getBubbleWithView(View view) { 753 for (int i = 0; i < mBubbles.size(); i++) { 754 Bubble bubble = mBubbles.get(i); 755 if (bubble.getIconView() != null && bubble.getIconView().equals(view)) { 756 return bubble; 757 } 758 } 759 return null; 760 } 761 762 @VisibleForTesting(visibility = PRIVATE) getOverflowBubbleWithKey(String key)763 Bubble getOverflowBubbleWithKey(String key) { 764 for (int i = 0; i < mOverflowBubbles.size(); i++) { 765 Bubble bubble = mOverflowBubbles.get(i); 766 if (bubble.getKey().equals(key)) { 767 return bubble; 768 } 769 } 770 return null; 771 } 772 773 @VisibleForTesting(visibility = PRIVATE) setTimeSource(TimeSource timeSource)774 void setTimeSource(TimeSource timeSource) { 775 mTimeSource = timeSource; 776 } 777 setListener(Listener listener)778 public void setListener(Listener listener) { 779 mListener = listener; 780 } 781 782 /** 783 * Set maximum number of bubbles allowed in overflow. 784 * This method should only be used in tests, not in production. 785 */ 786 @VisibleForTesting setMaxOverflowBubbles(int maxOverflowBubbles)787 void setMaxOverflowBubbles(int maxOverflowBubbles) { 788 mMaxOverflowBubbles = maxOverflowBubbles; 789 } 790 791 /** 792 * Description of current bubble data state. 793 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)794 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 795 pw.print("selected: "); 796 pw.println(mSelectedBubble != null 797 ? mSelectedBubble.getKey() 798 : "null"); 799 pw.print("expanded: "); 800 pw.println(mExpanded); 801 pw.print("count: "); 802 pw.println(mBubbles.size()); 803 for (Bubble bubble : mBubbles) { 804 bubble.dump(fd, pw, args); 805 } 806 pw.print("summaryKeys: "); 807 pw.println(mSuppressedGroupKeys.size()); 808 for (String key : mSuppressedGroupKeys.keySet()) { 809 pw.println(" suppressing: " + key); 810 } 811 } 812 } 813