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