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