• 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.BubbleDebugConfig.DEBUG_BUBBLE_DATA;
21 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
22 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
23 
24 import android.annotation.NonNull;
25 import android.app.PendingIntent;
26 import android.content.Context;
27 import android.content.LocusId;
28 import android.content.pm.ShortcutInfo;
29 import android.text.TextUtils;
30 import android.util.ArrayMap;
31 import android.util.ArraySet;
32 import android.util.Log;
33 import android.util.Pair;
34 import android.view.View;
35 
36 import androidx.annotation.Nullable;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.util.FrameworkStatsLog;
40 import com.android.wm.shell.R;
41 import com.android.wm.shell.bubbles.Bubbles.DismissReason;
42 
43 import java.io.FileDescriptor;
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     @Nullable
163     private Bubbles.SuppressionChangedListener mSuppressionListener;
164     private Bubbles.PendingIntentCanceledListener mCancelledListener;
165 
166     /**
167      * We track groups with summaries that aren't visibly displayed but still kept around because
168      * the bubble(s) associated with the summary still exist.
169      *
170      * The summary must be kept around so that developers can cancel it (and hence the bubbles
171      * associated with it). This list is used to check if the summary should be hidden from the
172      * shade.
173      *
174      * Key: group key of the notification
175      * Value: key of the notification
176      */
177     private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();
178 
BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner, Executor mainExecutor)179     public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner,
180             Executor mainExecutor) {
181         mContext = context;
182         mLogger = bubbleLogger;
183         mPositioner = positioner;
184         mMainExecutor = mainExecutor;
185         mOverflow = new BubbleOverflow(context, positioner);
186         mBubbles = new ArrayList<>();
187         mOverflowBubbles = new ArrayList<>();
188         mPendingBubbles = new HashMap<>();
189         mStateChange = new Update(mBubbles, mOverflowBubbles);
190         mMaxBubbles = mPositioner.getMaxBubbles();
191         mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
192     }
193 
setSuppressionChangedListener( Bubbles.SuppressionChangedListener listener)194     public void setSuppressionChangedListener(
195             Bubbles.SuppressionChangedListener listener) {
196         mSuppressionListener = listener;
197     }
198 
setPendingIntentCancelledListener( Bubbles.PendingIntentCanceledListener listener)199     public void setPendingIntentCancelledListener(
200             Bubbles.PendingIntentCanceledListener listener) {
201         mCancelledListener = listener;
202     }
203 
onMaxBubblesChanged()204     public void onMaxBubblesChanged() {
205         mMaxBubbles = mPositioner.getMaxBubbles();
206         if (!mExpanded) {
207             trim();
208             dispatchPendingChanges();
209         } else {
210             mNeedsTrimming = true;
211         }
212     }
213 
hasBubbles()214     public boolean hasBubbles() {
215         return !mBubbles.isEmpty();
216     }
217 
hasOverflowBubbles()218     public boolean hasOverflowBubbles() {
219         return !mOverflowBubbles.isEmpty();
220     }
221 
isExpanded()222     public boolean isExpanded() {
223         return mExpanded;
224     }
225 
hasAnyBubbleWithKey(String key)226     public boolean hasAnyBubbleWithKey(String key) {
227         return hasBubbleInStackWithKey(key) || hasOverflowBubbleWithKey(key);
228     }
229 
hasBubbleInStackWithKey(String key)230     public boolean hasBubbleInStackWithKey(String key) {
231         return getBubbleInStackWithKey(key) != null;
232     }
233 
hasOverflowBubbleWithKey(String key)234     public boolean hasOverflowBubbleWithKey(String key) {
235         return getOverflowBubbleWithKey(key) != null;
236     }
237 
238     @Nullable
getSelectedBubble()239     public BubbleViewProvider getSelectedBubble() {
240         return mSelectedBubble;
241     }
242 
getOverflow()243     public BubbleOverflow getOverflow() {
244         return mOverflow;
245     }
246 
247     /** Return a read-only current active bubble lists. */
getActiveBubbles()248     public List<Bubble> getActiveBubbles() {
249         return Collections.unmodifiableList(mBubbles);
250     }
251 
setExpanded(boolean expanded)252     public void setExpanded(boolean expanded) {
253         if (DEBUG_BUBBLE_DATA) {
254             Log.d(TAG, "setExpanded: " + expanded);
255         }
256         setExpandedInternal(expanded);
257         dispatchPendingChanges();
258     }
259 
setSelectedBubble(BubbleViewProvider bubble)260     public void setSelectedBubble(BubbleViewProvider bubble) {
261         if (DEBUG_BUBBLE_DATA) {
262             Log.d(TAG, "setSelectedBubble: " + bubble);
263         }
264         setSelectedBubbleInternal(bubble);
265         dispatchPendingChanges();
266     }
267 
setShowingOverflow(boolean showingOverflow)268     void setShowingOverflow(boolean showingOverflow) {
269         mShowingOverflow = showingOverflow;
270     }
271 
isShowingOverflow()272     boolean isShowingOverflow() {
273         return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar());
274     }
275 
276     /**
277      * Constructs a new bubble or returns an existing one. Does not add new bubbles to
278      * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
279      * for that.
280      *
281      * @param entry The notification entry to use, only null if it's a bubble being promoted from
282      *              the overflow that was persisted over reboot.
283      * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from
284      *              the overflow that was persisted over reboot.
285      */
getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble)286     public Bubble getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble) {
287         String key = persistedBubble != null ? persistedBubble.getKey() : entry.getKey();
288         Bubble bubbleToReturn = getBubbleInStackWithKey(key);
289 
290         if (bubbleToReturn == null) {
291             bubbleToReturn = getOverflowBubbleWithKey(key);
292             if (bubbleToReturn != null) {
293                 // Promoting from overflow
294                 mOverflowBubbles.remove(bubbleToReturn);
295             } else if (mPendingBubbles.containsKey(key)) {
296                 // Update while it was pending
297                 bubbleToReturn = mPendingBubbles.get(key);
298             } else if (entry != null) {
299                 // New bubble
300                 bubbleToReturn = new Bubble(entry, mSuppressionListener, mCancelledListener,
301                         mMainExecutor);
302             } else {
303                 // Persisted bubble being promoted
304                 bubbleToReturn = persistedBubble;
305             }
306         }
307 
308         if (entry != null) {
309             bubbleToReturn.setEntry(entry);
310         }
311         mPendingBubbles.put(key, bubbleToReturn);
312         return bubbleToReturn;
313     }
314 
315     /**
316      * When this method is called it is expected that all info in the bubble has completed loading.
317      * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView,
318      * BubbleIconFactory, boolean)
319      */
notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade)320     void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
321         if (DEBUG_BUBBLE_DATA) {
322             Log.d(TAG, "notificationEntryUpdated: " + bubble);
323         }
324         mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here
325         Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
326         suppressFlyout |= !bubble.isVisuallyInterruptive();
327 
328         if (prevBubble == null) {
329             // Create a new bubble
330             bubble.setSuppressFlyout(suppressFlyout);
331             doAdd(bubble);
332             trim();
333         } else {
334             // Updates an existing bubble
335             bubble.setSuppressFlyout(suppressFlyout);
336             // If there is no flyout, we probably shouldn't show the bubble at the top
337             doUpdate(bubble, !suppressFlyout /* reorder */);
338         }
339 
340         if (bubble.shouldAutoExpand()) {
341             bubble.setShouldAutoExpand(false);
342             setSelectedBubbleInternal(bubble);
343             if (!mExpanded) {
344                 setExpandedInternal(true);
345             }
346         }
347 
348         boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
349         boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
350         bubble.setSuppressNotification(suppress);
351         bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
352 
353         LocusId locusId = bubble.getLocusId();
354         if (locusId != null) {
355             boolean isSuppressed = mSuppressedBubbles.containsKey(locusId);
356             if (isSuppressed && (!bubble.isSuppressed() || !bubble.isSuppressable())) {
357                 mSuppressedBubbles.remove(locusId);
358                 mStateChange.unsuppressedBubble = bubble;
359             } else if (!isSuppressed && (bubble.isSuppressed()
360                     || bubble.isSuppressable() && mVisibleLocusIds.contains(locusId))) {
361                 mSuppressedBubbles.put(locusId, bubble);
362                 mStateChange.suppressedBubble = bubble;
363             }
364         }
365         dispatchPendingChanges();
366     }
367 
368     /**
369      * Dismisses the bubble with the matching key, if it exists.
370      */
dismissBubbleWithKey(String key, @DismissReason int reason)371     public void dismissBubbleWithKey(String key, @DismissReason int reason) {
372         if (DEBUG_BUBBLE_DATA) {
373             Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
374         }
375         doRemove(key, reason);
376         dispatchPendingChanges();
377     }
378 
379     /**
380      * Adds a group key indicating that the summary for this group should be suppressed.
381      *
382      * @param groupKey the group key of the group whose summary should be suppressed.
383      * @param notifKey the notification entry key of that summary.
384      */
addSummaryToSuppress(String groupKey, String notifKey)385     void addSummaryToSuppress(String groupKey, String notifKey) {
386         mSuppressedGroupKeys.put(groupKey, notifKey);
387         mStateChange.suppressedSummaryChanged = true;
388         mStateChange.suppressedSummaryGroup = groupKey;
389         dispatchPendingChanges();
390     }
391 
392     /**
393      * Retrieves the notif entry key of the summary associated with the provided group key.
394      *
395      * @param groupKey the group to look up
396      * @return the key for the notification that is the summary of this group.
397      */
getSummaryKey(String groupKey)398     String getSummaryKey(String groupKey) {
399         return mSuppressedGroupKeys.get(groupKey);
400     }
401 
402     /**
403      * Removes a group key indicating that summary for this group should no longer be suppressed.
404      */
removeSuppressedSummary(String groupKey)405     void removeSuppressedSummary(String groupKey) {
406         mSuppressedGroupKeys.remove(groupKey);
407         mStateChange.suppressedSummaryChanged = true;
408         mStateChange.suppressedSummaryGroup = groupKey;
409         dispatchPendingChanges();
410     }
411 
412     /**
413      * Whether the summary for the provided group key is suppressed.
414      */
415     @VisibleForTesting
isSummarySuppressed(String groupKey)416     public boolean isSummarySuppressed(String groupKey) {
417         return mSuppressedGroupKeys.containsKey(groupKey);
418     }
419 
420     /**
421      * Removes bubbles from the given package whose shortcut are not in the provided list of valid
422      * shortcuts.
423      */
removeBubblesWithInvalidShortcuts( String packageName, List<ShortcutInfo> validShortcuts, int reason)424     public void removeBubblesWithInvalidShortcuts(
425             String packageName, List<ShortcutInfo> validShortcuts, int reason) {
426 
427         final Set<String> validShortcutIds = new HashSet<String>();
428         for (ShortcutInfo info : validShortcuts) {
429             validShortcutIds.add(info.getId());
430         }
431 
432         final Predicate<Bubble> invalidBubblesFromPackage = bubble -> {
433             final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName());
434             final boolean isShortcutBubble = bubble.hasMetadataShortcutId();
435             if (!bubbleIsFromPackage || !isShortcutBubble) {
436                 return false;
437             }
438             final boolean hasShortcutIdAndValidShortcut =
439                     bubble.hasMetadataShortcutId()
440                             && bubble.getShortcutInfo() != null
441                             && bubble.getShortcutInfo().isEnabled()
442                             && validShortcutIds.contains(bubble.getShortcutInfo().getId());
443             return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut;
444         };
445 
446         final Consumer<Bubble> removeBubble = bubble ->
447                 dismissBubbleWithKey(bubble.getKey(), reason);
448 
449         performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble);
450         performActionOnBubblesMatching(
451                 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble);
452     }
453 
454     /** Dismisses all bubbles from the given package. */
removeBubblesWithPackageName(String packageName, int reason)455     public void removeBubblesWithPackageName(String packageName, int reason) {
456         final Predicate<Bubble> bubbleMatchesPackage = bubble ->
457                 bubble.getPackageName().equals(packageName);
458 
459         final Consumer<Bubble> removeBubble = bubble ->
460                 dismissBubbleWithKey(bubble.getKey(), reason);
461 
462         performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble);
463         performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble);
464     }
465 
doAdd(Bubble bubble)466     private void doAdd(Bubble bubble) {
467         if (DEBUG_BUBBLE_DATA) {
468             Log.d(TAG, "doAdd: " + bubble);
469         }
470         mBubbles.add(0, bubble);
471         mStateChange.addedBubble = bubble;
472         // Adding the first bubble doesn't change the order
473         mStateChange.orderChanged = mBubbles.size() > 1;
474         if (!isExpanded()) {
475             setSelectedBubbleInternal(mBubbles.get(0));
476         }
477     }
478 
trim()479     private void trim() {
480         if (mBubbles.size() > mMaxBubbles) {
481             int numtoRemove = mBubbles.size() - mMaxBubbles;
482             ArrayList<Bubble> toRemove = new ArrayList<>();
483             mBubbles.stream()
484                     // sort oldest first (ascending lastActivity)
485                     .sorted(Comparator.comparingLong(Bubble::getLastActivity))
486                     // skip the selected bubble
487                     .filter((b) -> !b.equals(mSelectedBubble))
488                     .forEachOrdered((b) -> {
489                         if (toRemove.size() < numtoRemove) {
490                             toRemove.add(b);
491                         }
492                     });
493             toRemove.forEach((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED));
494         }
495     }
496 
doUpdate(Bubble bubble, boolean reorder)497     private void doUpdate(Bubble bubble, boolean reorder) {
498         if (DEBUG_BUBBLE_DATA) {
499             Log.d(TAG, "doUpdate: " + bubble);
500         }
501         mStateChange.updatedBubble = bubble;
502         if (!isExpanded() && reorder) {
503             int prevPos = mBubbles.indexOf(bubble);
504             mBubbles.remove(bubble);
505             mBubbles.add(0, bubble);
506             mStateChange.orderChanged = prevPos != 0;
507             setSelectedBubbleInternal(mBubbles.get(0));
508         }
509     }
510 
511     /** Runs the given action on Bubbles that match the given predicate. */
performActionOnBubblesMatching( List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action)512     private void performActionOnBubblesMatching(
513             List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) {
514         final List<Bubble> matchingBubbles = new ArrayList<>();
515         for (Bubble bubble : bubbles) {
516             if (predicate.test(bubble)) {
517                 matchingBubbles.add(bubble);
518             }
519         }
520 
521         for (Bubble matchingBubble : matchingBubbles) {
522             action.accept(matchingBubble);
523         }
524     }
525 
doRemove(String key, @DismissReason int reason)526     private void doRemove(String key, @DismissReason int reason) {
527         if (DEBUG_BUBBLE_DATA) {
528             Log.d(TAG, "doRemove: " + key);
529         }
530         //  If it was pending remove it
531         if (mPendingBubbles.containsKey(key)) {
532             mPendingBubbles.remove(key);
533         }
534         int indexToRemove = indexForKey(key);
535         if (indexToRemove == -1) {
536             if (hasOverflowBubbleWithKey(key)
537                     && (reason == Bubbles.DISMISS_NOTIF_CANCEL
538                         || reason == Bubbles.DISMISS_GROUP_CANCELLED
539                         || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE
540                         || reason == Bubbles.DISMISS_BLOCKED
541                         || reason == Bubbles.DISMISS_SHORTCUT_REMOVED
542                         || reason == Bubbles.DISMISS_PACKAGE_REMOVED
543                         || reason == Bubbles.DISMISS_USER_CHANGED)) {
544 
545                 Bubble b = getOverflowBubbleWithKey(key);
546                 if (DEBUG_BUBBLE_DATA) {
547                     Log.d(TAG, "Cancel overflow bubble: " + b);
548                 }
549                 if (b != null) {
550                     b.stopInflation();
551                 }
552                 mLogger.logOverflowRemove(b, reason);
553                 mOverflowBubbles.remove(b);
554                 mStateChange.bubbleRemoved(b, reason);
555                 mStateChange.removedOverflowBubble = b;
556             }
557             return;
558         }
559         Bubble bubbleToRemove = mBubbles.get(indexToRemove);
560         bubbleToRemove.stopInflation();
561         if (mBubbles.size() == 1) {
562             if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) {
563                 // No more active bubbles but we have stuff in the overflow -- select that view
564                 // if we're already expanded or always showing.
565                 setShowingOverflow(true);
566                 setSelectedBubbleInternal(mOverflow);
567             } else {
568                 setExpandedInternal(false);
569                 // Don't use setSelectedBubbleInternal because we don't want to trigger an
570                 // applyUpdate
571                 mSelectedBubble = null;
572             }
573         }
574         if (indexToRemove < mBubbles.size() - 1) {
575             // Removing anything but the last bubble means positions will change.
576             mStateChange.orderChanged = true;
577         }
578         mBubbles.remove(indexToRemove);
579         mStateChange.bubbleRemoved(bubbleToRemove, reason);
580         if (!isExpanded()) {
581             mStateChange.orderChanged |= repackAll();
582         }
583 
584         overflowBubble(reason, bubbleToRemove);
585 
586         // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
587         if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
588             // Move selection to the new bubble at the same position.
589             int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
590             BubbleViewProvider newSelected = mBubbles.get(newIndex);
591             setSelectedBubbleInternal(newSelected);
592         }
593         maybeSendDeleteIntent(reason, bubbleToRemove);
594     }
595 
overflowBubble(@ismissReason int reason, Bubble bubble)596     void overflowBubble(@DismissReason int reason, Bubble bubble) {
597         if (bubble.getPendingIntentCanceled()
598                 || !(reason == Bubbles.DISMISS_AGED
599                     || reason == Bubbles.DISMISS_USER_GESTURE
600                     || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
601             return;
602         }
603         if (DEBUG_BUBBLE_DATA) {
604             Log.d(TAG, "Overflowing: " + bubble);
605         }
606         mLogger.logOverflowAdd(bubble, reason);
607         mOverflowBubbles.remove(bubble);
608         mOverflowBubbles.add(0, bubble);
609         mStateChange.addedOverflowBubble = bubble;
610         bubble.stopInflation();
611         if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
612             // Remove oldest bubble.
613             Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1);
614             if (DEBUG_BUBBLE_DATA) {
615                 Log.d(TAG, "Overflow full. Remove: " + oldest);
616             }
617             mStateChange.bubbleRemoved(oldest, Bubbles.DISMISS_OVERFLOW_MAX_REACHED);
618             mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED);
619             mOverflowBubbles.remove(oldest);
620             mStateChange.removedOverflowBubble = oldest;
621         }
622     }
623 
dismissAll(@ismissReason int reason)624     public void dismissAll(@DismissReason int reason) {
625         if (DEBUG_BUBBLE_DATA) {
626             Log.d(TAG, "dismissAll: reason=" + reason);
627         }
628         if (mBubbles.isEmpty()) {
629             return;
630         }
631         setExpandedInternal(false);
632         setSelectedBubbleInternal(null);
633         while (!mBubbles.isEmpty()) {
634             doRemove(mBubbles.get(0).getKey(), reason);
635         }
636         dispatchPendingChanges();
637     }
638 
639     /**
640      * Called in response to the visibility of a locusId changing. A locusId is set on a task
641      * and if there's a matching bubble for that locusId then the bubble may be hidden or shown
642      * depending on the visibility of the locusId.
643      *
644      * @param taskId the taskId associated with the locusId visibility change.
645      * @param locusId the locusId whose visibility has changed.
646      * @param visible whether the task with the locusId is visible or not.
647      */
onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible)648     public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) {
649         Bubble matchingBubble = getBubbleInStackWithLocusId(locusId);
650         // Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled.
651         if (visible && (matchingBubble == null || matchingBubble.getTaskId() != taskId)) {
652             mVisibleLocusIds.add(locusId);
653         } else {
654             mVisibleLocusIds.remove(locusId);
655         }
656         if (matchingBubble == null) {
657             return;
658         }
659         boolean isAlreadySuppressed = mSuppressedBubbles.get(locusId) != null;
660         if (visible && !isAlreadySuppressed && matchingBubble.isSuppressable()
661                 && taskId != matchingBubble.getTaskId()) {
662             mSuppressedBubbles.put(locusId, matchingBubble);
663             matchingBubble.setSuppressBubble(true);
664             mStateChange.suppressedBubble = matchingBubble;
665             dispatchPendingChanges();
666         } else if (!visible) {
667             Bubble unsuppressedBubble = mSuppressedBubbles.remove(locusId);
668             if (unsuppressedBubble != null) {
669                 unsuppressedBubble.setSuppressBubble(false);
670                 mStateChange.unsuppressedBubble = unsuppressedBubble;
671             }
672             dispatchPendingChanges();
673         }
674     }
675 
676     /**
677      * Removes all bubbles from the overflow, called when the user changes.
678      */
clearOverflow()679     public void clearOverflow() {
680         while (!mOverflowBubbles.isEmpty()) {
681             doRemove(mOverflowBubbles.get(0).getKey(), Bubbles.DISMISS_USER_CHANGED);
682         }
683         dispatchPendingChanges();
684     }
685 
dispatchPendingChanges()686     private void dispatchPendingChanges() {
687         if (mListener != null && mStateChange.anythingChanged()) {
688             mListener.applyUpdate(mStateChange);
689         }
690         mStateChange = new Update(mBubbles, mOverflowBubbles);
691     }
692 
693     /**
694      * Requests a change to the selected bubble.
695      *
696      * @param bubble the new selected bubble
697      */
setSelectedBubbleInternal(@ullable BubbleViewProvider bubble)698     private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) {
699         if (DEBUG_BUBBLE_DATA) {
700             Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
701         }
702         if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) {
703             return;
704         }
705         // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.
706         boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey());
707         if (bubble != null
708                 && !mBubbles.contains(bubble)
709                 && !mOverflowBubbles.contains(bubble)
710                 && !isOverflow) {
711             Log.e(TAG, "Cannot select bubble which doesn't exist!"
712                     + " (" + bubble + ") bubbles=" + mBubbles);
713             return;
714         }
715         if (mExpanded && bubble != null && !isOverflow) {
716             ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
717         }
718         mSelectedBubble = bubble;
719         mStateChange.selectedBubble = bubble;
720         mStateChange.selectionChanged = true;
721     }
722 
setCurrentUserId(int uid)723     void setCurrentUserId(int uid) {
724         mCurrentUserId = uid;
725     }
726 
727     /**
728      * Logs the bubble UI event.
729      *
730      * @param provider The bubble view provider that is being interacted on. Null value indicates
731      *               that the user interaction is not specific to one bubble.
732      * @param action The user interaction enum
733      * @param packageName SystemUI package
734      * @param bubbleCount Number of bubbles in the stack
735      * @param bubbleIndex Index of bubble in the stack
736      * @param normalX Normalized x position of the stack
737      * @param normalY Normalized y position of the stack
738      */
logBubbleEvent(@ullable BubbleViewProvider provider, int action, String packageName, int bubbleCount, int bubbleIndex, float normalX, float normalY)739     void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName,
740             int bubbleCount, int bubbleIndex, float normalX, float normalY) {
741         if (provider == null) {
742             mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY);
743         } else if (provider.getKey().equals(BubbleOverflow.KEY)) {
744             if (action == FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) {
745                 mLogger.logShowOverflow(packageName, mCurrentUserId);
746             }
747         } else {
748             mLogger.logBubbleUiChanged((Bubble) provider, packageName, action, bubbleCount, normalX,
749                     normalY, bubbleIndex);
750         }
751     }
752 
753     /**
754      * Requests a change to the expanded state.
755      *
756      * @param shouldExpand the new requested state
757      */
setExpandedInternal(boolean shouldExpand)758     private void setExpandedInternal(boolean shouldExpand) {
759         if (DEBUG_BUBBLE_DATA) {
760             Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand);
761         }
762         if (mExpanded == shouldExpand) {
763             return;
764         }
765         if (shouldExpand) {
766             if (mBubbles.isEmpty() && !mShowingOverflow) {
767                 Log.e(TAG, "Attempt to expand stack when empty!");
768                 return;
769             }
770             if (mSelectedBubble == null) {
771                 Log.e(TAG, "Attempt to expand stack without selected bubble!");
772                 return;
773             }
774             if (mSelectedBubble instanceof Bubble) {
775                 ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
776             }
777             mStateChange.orderChanged |= repackAll();
778         } else if (!mBubbles.isEmpty()) {
779             // Apply ordering and grouping rules from expanded -> collapsed, then save
780             // the result.
781             mStateChange.orderChanged |= repackAll();
782             // Save the state which should be returned to when expanded (with no other changes)
783 
784             if (mShowingOverflow) {
785                 // Show previously selected bubble instead of overflow menu on next expansion.
786                 if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) {
787                     setSelectedBubbleInternal(mSelectedBubble);
788                 } else {
789                     setSelectedBubbleInternal(mBubbles.get(0));
790                 }
791             }
792             if (mBubbles.indexOf(mSelectedBubble) > 0) {
793                 // Move the selected bubble to the top while collapsed.
794                 int index = mBubbles.indexOf(mSelectedBubble);
795                 if (index != 0) {
796                     mBubbles.remove((Bubble) mSelectedBubble);
797                     mBubbles.add(0, (Bubble) mSelectedBubble);
798                     mStateChange.orderChanged = true;
799                 }
800             }
801         }
802         if (mNeedsTrimming) {
803             mNeedsTrimming = false;
804             trim();
805         }
806         mExpanded = shouldExpand;
807         mStateChange.expanded = shouldExpand;
808         mStateChange.expandedChanged = true;
809     }
810 
sortKey(Bubble bubble)811     private static long sortKey(Bubble bubble) {
812         return bubble.getLastActivity();
813     }
814 
815     /**
816      * This applies a full sort and group pass to all existing bubbles.
817      * Bubbles are sorted by lastUpdated descending.
818      *
819      * @return true if the position of any bubbles changed as a result
820      */
repackAll()821     private boolean repackAll() {
822         if (DEBUG_BUBBLE_DATA) {
823             Log.d(TAG, "repackAll()");
824         }
825         if (mBubbles.isEmpty()) {
826             return false;
827         }
828         List<Bubble> repacked = new ArrayList<>(mBubbles.size());
829         // Add bubbles, freshest to oldest
830         mBubbles.stream()
831                 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
832                 .forEachOrdered(repacked::add);
833         if (repacked.equals(mBubbles)) {
834             return false;
835         }
836         mBubbles.clear();
837         mBubbles.addAll(repacked);
838         return true;
839     }
840 
maybeSendDeleteIntent(@ismissReason int reason, @NonNull final Bubble bubble)841     private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) {
842         if (reason != Bubbles.DISMISS_USER_GESTURE) return;
843         PendingIntent deleteIntent = bubble.getDeleteIntent();
844         if (deleteIntent == null) return;
845         try {
846             deleteIntent.send();
847         } catch (PendingIntent.CanceledException e) {
848             Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey());
849         }
850     }
851 
indexForKey(String key)852     private int indexForKey(String key) {
853         for (int i = 0; i < mBubbles.size(); i++) {
854             Bubble bubble = mBubbles.get(i);
855             if (bubble.getKey().equals(key)) {
856                 return i;
857             }
858         }
859         return -1;
860     }
861 
862     /**
863      * The set of bubbles in row.
864      */
865     @VisibleForTesting(visibility = PACKAGE)
getBubbles()866     public List<Bubble> getBubbles() {
867         return Collections.unmodifiableList(mBubbles);
868     }
869 
870     /**
871      * The set of bubbles in overflow.
872      */
873     @VisibleForTesting(visibility = PRIVATE)
getOverflowBubbles()874     public List<Bubble> getOverflowBubbles() {
875         return Collections.unmodifiableList(mOverflowBubbles);
876     }
877 
878     @VisibleForTesting(visibility = PRIVATE)
879     @Nullable
getAnyBubbleWithkey(String key)880     Bubble getAnyBubbleWithkey(String key) {
881         Bubble b = getBubbleInStackWithKey(key);
882         if (b == null) {
883             b = getOverflowBubbleWithKey(key);
884         }
885         return b;
886     }
887 
888     /** @return any bubble (in the stack or the overflow) that matches the provided shortcutId. */
889     @Nullable
getAnyBubbleWithShortcutId(String shortcutId)890     Bubble getAnyBubbleWithShortcutId(String shortcutId) {
891         if (TextUtils.isEmpty(shortcutId)) {
892             return null;
893         }
894         for (int i = 0; i < mBubbles.size(); i++) {
895             Bubble bubble = mBubbles.get(i);
896             String bubbleShortcutId = bubble.getShortcutInfo() != null
897                     ? bubble.getShortcutInfo().getId()
898                     : bubble.getMetadataShortcutId();
899             if (shortcutId.equals(bubbleShortcutId)) {
900                 return bubble;
901             }
902         }
903 
904         for (int i = 0; i < mOverflowBubbles.size(); i++) {
905             Bubble bubble = mOverflowBubbles.get(i);
906             String bubbleShortcutId = bubble.getShortcutInfo() != null
907                     ? bubble.getShortcutInfo().getId()
908                     : bubble.getMetadataShortcutId();
909             if (shortcutId.equals(bubbleShortcutId)) {
910                 return bubble;
911             }
912         }
913         return null;
914     }
915 
916     @VisibleForTesting(visibility = PRIVATE)
917     @Nullable
getBubbleInStackWithKey(String key)918     public Bubble getBubbleInStackWithKey(String key) {
919         for (int i = 0; i < mBubbles.size(); i++) {
920             Bubble bubble = mBubbles.get(i);
921             if (bubble.getKey().equals(key)) {
922                 return bubble;
923             }
924         }
925         return null;
926     }
927 
928     @Nullable
getBubbleInStackWithLocusId(LocusId locusId)929     private Bubble getBubbleInStackWithLocusId(LocusId locusId) {
930         if (locusId == null) return null;
931         for (int i = 0; i < mBubbles.size(); i++) {
932             Bubble bubble = mBubbles.get(i);
933             if (locusId.equals(bubble.getLocusId())) {
934                 return bubble;
935             }
936         }
937         return null;
938     }
939 
940     @Nullable
getBubbleWithView(View view)941     Bubble getBubbleWithView(View view) {
942         for (int i = 0; i < mBubbles.size(); i++) {
943             Bubble bubble = mBubbles.get(i);
944             if (bubble.getIconView() != null && bubble.getIconView().equals(view)) {
945                 return bubble;
946             }
947         }
948         return null;
949     }
950 
951     @VisibleForTesting(visibility = PRIVATE)
getOverflowBubbleWithKey(String key)952     public Bubble getOverflowBubbleWithKey(String key) {
953         for (int i = 0; i < mOverflowBubbles.size(); i++) {
954             Bubble bubble = mOverflowBubbles.get(i);
955             if (bubble.getKey().equals(key)) {
956                 return bubble;
957             }
958         }
959         return null;
960     }
961 
962     @VisibleForTesting(visibility = PRIVATE)
setTimeSource(TimeSource timeSource)963     void setTimeSource(TimeSource timeSource) {
964         mTimeSource = timeSource;
965     }
966 
setListener(Listener listener)967     public void setListener(Listener listener) {
968         mListener = listener;
969     }
970 
971     /**
972      * Set maximum number of bubbles allowed in overflow.
973      * This method should only be used in tests, not in production.
974      */
975     @VisibleForTesting
setMaxOverflowBubbles(int maxOverflowBubbles)976     public void setMaxOverflowBubbles(int maxOverflowBubbles) {
977         mMaxOverflowBubbles = maxOverflowBubbles;
978     }
979 
980     /**
981      * Description of current bubble data state.
982      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)983     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
984         pw.print("selected: ");
985         pw.println(mSelectedBubble != null
986                 ? mSelectedBubble.getKey()
987                 : "null");
988         pw.print("expanded: ");
989         pw.println(mExpanded);
990 
991         pw.print("stack bubble count:    ");
992         pw.println(mBubbles.size());
993         for (Bubble bubble : mBubbles) {
994             bubble.dump(fd, pw, args);
995         }
996 
997         pw.print("overflow bubble count:    ");
998         pw.println(mOverflowBubbles.size());
999         for (Bubble bubble : mOverflowBubbles) {
1000             bubble.dump(fd, pw, args);
1001         }
1002 
1003         pw.print("summaryKeys: ");
1004         pw.println(mSuppressedGroupKeys.size());
1005         for (String key : mSuppressedGroupKeys.keySet()) {
1006             pw.println("   suppressing: " + key);
1007         }
1008     }
1009 }
1010