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