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