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