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