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.isTextChanged();
327 
328         if (prevBubble == null) {
329             // Create a new bubble
330             bubble.setSuppressFlyout(suppressFlyout);
331             bubble.markUpdatedAt(mTimeSource.currentTimeMillis());
332             doAdd(bubble);
333             trim();
334         } else {
335             // Updates an existing bubble
336             bubble.setSuppressFlyout(suppressFlyout);
337             // If there is no flyout, we probably shouldn't show the bubble at the top
338             doUpdate(bubble, !suppressFlyout /* reorder */);
339         }
340 
341         if (bubble.shouldAutoExpand()) {
342             bubble.setShouldAutoExpand(false);
343             setSelectedBubbleInternal(bubble);
344             if (!mExpanded) {
345                 setExpandedInternal(true);
346             }
347         }
348 
349         boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
350         boolean suppress = isBubbleExpandedAndSelected || !showInShade || !bubble.showInShade();
351         bubble.setSuppressNotification(suppress);
352         bubble.setShowDot(!isBubbleExpandedAndSelected /* show */);
353 
354         LocusId locusId = bubble.getLocusId();
355         if (locusId != null) {
356             boolean isSuppressed = mSuppressedBubbles.containsKey(locusId);
357             if (isSuppressed && (!bubble.isSuppressed() || !bubble.isSuppressable())) {
358                 mSuppressedBubbles.remove(locusId);
359                 mStateChange.unsuppressedBubble = bubble;
360             } else if (!isSuppressed && (bubble.isSuppressed()
361                     || bubble.isSuppressable() && mVisibleLocusIds.contains(locusId))) {
362                 mSuppressedBubbles.put(locusId, bubble);
363                 mStateChange.suppressedBubble = bubble;
364             }
365         }
366         dispatchPendingChanges();
367     }
368 
369     /**
370      * Dismisses the bubble with the matching key, if it exists.
371      */
dismissBubbleWithKey(String key, @DismissReason int reason)372     public void dismissBubbleWithKey(String key, @DismissReason int reason) {
373         if (DEBUG_BUBBLE_DATA) {
374             Log.d(TAG, "notificationEntryRemoved: key=" + key + " reason=" + reason);
375         }
376         doRemove(key, reason);
377         dispatchPendingChanges();
378     }
379 
380     /**
381      * Adds a group key indicating that the summary for this group should be suppressed.
382      *
383      * @param groupKey the group key of the group whose summary should be suppressed.
384      * @param notifKey the notification entry key of that summary.
385      */
addSummaryToSuppress(String groupKey, String notifKey)386     void addSummaryToSuppress(String groupKey, String notifKey) {
387         mSuppressedGroupKeys.put(groupKey, notifKey);
388         mStateChange.suppressedSummaryChanged = true;
389         mStateChange.suppressedSummaryGroup = groupKey;
390         dispatchPendingChanges();
391     }
392 
393     /**
394      * Retrieves the notif entry key of the summary associated with the provided group key.
395      *
396      * @param groupKey the group to look up
397      * @return the key for the notification that is the summary of this group.
398      */
getSummaryKey(String groupKey)399     String getSummaryKey(String groupKey) {
400         return mSuppressedGroupKeys.get(groupKey);
401     }
402 
403     /**
404      * Removes a group key indicating that summary for this group should no longer be suppressed.
405      */
removeSuppressedSummary(String groupKey)406     void removeSuppressedSummary(String groupKey) {
407         mSuppressedGroupKeys.remove(groupKey);
408         mStateChange.suppressedSummaryChanged = true;
409         mStateChange.suppressedSummaryGroup = groupKey;
410         dispatchPendingChanges();
411     }
412 
413     /**
414      * Whether the summary for the provided group key is suppressed.
415      */
416     @VisibleForTesting
isSummarySuppressed(String groupKey)417     public boolean isSummarySuppressed(String groupKey) {
418         return mSuppressedGroupKeys.containsKey(groupKey);
419     }
420 
421     /**
422      * Removes bubbles from the given package whose shortcut are not in the provided list of valid
423      * shortcuts.
424      */
removeBubblesWithInvalidShortcuts( String packageName, List<ShortcutInfo> validShortcuts, int reason)425     public void removeBubblesWithInvalidShortcuts(
426             String packageName, List<ShortcutInfo> validShortcuts, int reason) {
427 
428         final Set<String> validShortcutIds = new HashSet<String>();
429         for (ShortcutInfo info : validShortcuts) {
430             validShortcutIds.add(info.getId());
431         }
432 
433         final Predicate<Bubble> invalidBubblesFromPackage = bubble -> {
434             final boolean bubbleIsFromPackage = packageName.equals(bubble.getPackageName());
435             final boolean isShortcutBubble = bubble.hasMetadataShortcutId();
436             if (!bubbleIsFromPackage || !isShortcutBubble) {
437                 return false;
438             }
439             final boolean hasShortcutIdAndValidShortcut =
440                     bubble.hasMetadataShortcutId()
441                             && bubble.getShortcutInfo() != null
442                             && bubble.getShortcutInfo().isEnabled()
443                             && validShortcutIds.contains(bubble.getShortcutInfo().getId());
444             return bubbleIsFromPackage && !hasShortcutIdAndValidShortcut;
445         };
446 
447         final Consumer<Bubble> removeBubble = bubble ->
448                 dismissBubbleWithKey(bubble.getKey(), reason);
449 
450         performActionOnBubblesMatching(getBubbles(), invalidBubblesFromPackage, removeBubble);
451         performActionOnBubblesMatching(
452                 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble);
453     }
454 
455     /** Dismisses all bubbles from the given package. */
removeBubblesWithPackageName(String packageName, int reason)456     public void removeBubblesWithPackageName(String packageName, int reason) {
457         final Predicate<Bubble> bubbleMatchesPackage = bubble ->
458                 bubble.getPackageName().equals(packageName);
459 
460         final Consumer<Bubble> removeBubble = bubble ->
461                 dismissBubbleWithKey(bubble.getKey(), reason);
462 
463         performActionOnBubblesMatching(getBubbles(), bubbleMatchesPackage, removeBubble);
464         performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble);
465     }
466 
doAdd(Bubble bubble)467     private void doAdd(Bubble bubble) {
468         if (DEBUG_BUBBLE_DATA) {
469             Log.d(TAG, "doAdd: " + bubble);
470         }
471         mBubbles.add(0, bubble);
472         mStateChange.addedBubble = bubble;
473         // Adding the first bubble doesn't change the order
474         mStateChange.orderChanged = mBubbles.size() > 1;
475         if (!isExpanded()) {
476             setSelectedBubbleInternal(mBubbles.get(0));
477         }
478     }
479 
trim()480     private void trim() {
481         if (mBubbles.size() > mMaxBubbles) {
482             int numtoRemove = mBubbles.size() - mMaxBubbles;
483             ArrayList<Bubble> toRemove = new ArrayList<>();
484             mBubbles.stream()
485                     // sort oldest first (ascending lastActivity)
486                     .sorted(Comparator.comparingLong(Bubble::getLastActivity))
487                     // skip the selected bubble
488                     .filter((b) -> !b.equals(mSelectedBubble))
489                     .forEachOrdered((b) -> {
490                         if (toRemove.size() < numtoRemove) {
491                             toRemove.add(b);
492                         }
493                     });
494             toRemove.forEach((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED));
495         }
496     }
497 
doUpdate(Bubble bubble, boolean reorder)498     private void doUpdate(Bubble bubble, boolean reorder) {
499         if (DEBUG_BUBBLE_DATA) {
500             Log.d(TAG, "doUpdate: " + bubble);
501         }
502         mStateChange.updatedBubble = bubble;
503         if (!isExpanded() && reorder) {
504             int prevPos = mBubbles.indexOf(bubble);
505             mBubbles.remove(bubble);
506             mBubbles.add(0, bubble);
507             mStateChange.orderChanged = prevPos != 0;
508             setSelectedBubbleInternal(mBubbles.get(0));
509         }
510     }
511 
512     /** Runs the given action on Bubbles that match the given predicate. */
performActionOnBubblesMatching( List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action)513     private void performActionOnBubblesMatching(
514             List<Bubble> bubbles, Predicate<Bubble> predicate, Consumer<Bubble> action) {
515         final List<Bubble> matchingBubbles = new ArrayList<>();
516         for (Bubble bubble : bubbles) {
517             if (predicate.test(bubble)) {
518                 matchingBubbles.add(bubble);
519             }
520         }
521 
522         for (Bubble matchingBubble : matchingBubbles) {
523             action.accept(matchingBubble);
524         }
525     }
526 
doRemove(String key, @DismissReason int reason)527     private void doRemove(String key, @DismissReason int reason) {
528         if (DEBUG_BUBBLE_DATA) {
529             Log.d(TAG, "doRemove: " + key);
530         }
531         //  If it was pending remove it
532         if (mPendingBubbles.containsKey(key)) {
533             mPendingBubbles.remove(key);
534         }
535         int indexToRemove = indexForKey(key);
536         if (indexToRemove == -1) {
537             if (hasOverflowBubbleWithKey(key)
538                     && (reason == Bubbles.DISMISS_NOTIF_CANCEL
539                         || reason == Bubbles.DISMISS_GROUP_CANCELLED
540                         || reason == Bubbles.DISMISS_NO_LONGER_BUBBLE
541                         || reason == Bubbles.DISMISS_BLOCKED
542                         || reason == Bubbles.DISMISS_SHORTCUT_REMOVED
543                         || reason == Bubbles.DISMISS_PACKAGE_REMOVED
544                         || reason == Bubbles.DISMISS_USER_CHANGED)) {
545 
546                 Bubble b = getOverflowBubbleWithKey(key);
547                 if (DEBUG_BUBBLE_DATA) {
548                     Log.d(TAG, "Cancel overflow bubble: " + b);
549                 }
550                 if (b != null) {
551                     b.stopInflation();
552                 }
553                 mLogger.logOverflowRemove(b, reason);
554                 mOverflowBubbles.remove(b);
555                 mStateChange.bubbleRemoved(b, reason);
556                 mStateChange.removedOverflowBubble = b;
557             }
558             return;
559         }
560         Bubble bubbleToRemove = mBubbles.get(indexToRemove);
561         bubbleToRemove.stopInflation();
562         overflowBubble(reason, bubbleToRemove);
563 
564         if (mBubbles.size() == 1) {
565             if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) {
566                 // No more active bubbles but we have stuff in the overflow -- select that view
567                 // if we're already expanded or always showing.
568                 setShowingOverflow(true);
569                 setSelectedBubbleInternal(mOverflow);
570             } else {
571                 setExpandedInternal(false);
572                 // Don't use setSelectedBubbleInternal because we don't want to trigger an
573                 // applyUpdate
574                 mSelectedBubble = null;
575             }
576         }
577         if (indexToRemove < mBubbles.size() - 1) {
578             // Removing anything but the last bubble means positions will change.
579             mStateChange.orderChanged = true;
580         }
581         mBubbles.remove(indexToRemove);
582         mStateChange.bubbleRemoved(bubbleToRemove, reason);
583         if (!isExpanded()) {
584             mStateChange.orderChanged |= repackAll();
585         }
586 
587         // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
588         if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
589             // Move selection to the new bubble at the same position.
590             int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
591             BubbleViewProvider newSelected = mBubbles.get(newIndex);
592             setSelectedBubbleInternal(newSelected);
593         }
594         maybeSendDeleteIntent(reason, bubbleToRemove);
595     }
596 
overflowBubble(@ismissReason int reason, Bubble bubble)597     void overflowBubble(@DismissReason int reason, Bubble bubble) {
598         if (bubble.getPendingIntentCanceled()
599                 || !(reason == Bubbles.DISMISS_AGED
600                     || reason == Bubbles.DISMISS_USER_GESTURE
601                     || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) {
602             return;
603         }
604         if (DEBUG_BUBBLE_DATA) {
605             Log.d(TAG, "Overflowing: " + bubble);
606         }
607         mLogger.logOverflowAdd(bubble, reason);
608         mOverflowBubbles.remove(bubble);
609         mOverflowBubbles.add(0, bubble);
610         mStateChange.addedOverflowBubble = bubble;
611         bubble.stopInflation();
612         if (mOverflowBubbles.size() == mMaxOverflowBubbles + 1) {
613             // Remove oldest bubble.
614             Bubble oldest = mOverflowBubbles.get(mOverflowBubbles.size() - 1);
615             if (DEBUG_BUBBLE_DATA) {
616                 Log.d(TAG, "Overflow full. Remove: " + oldest);
617             }
618             mStateChange.bubbleRemoved(oldest, Bubbles.DISMISS_OVERFLOW_MAX_REACHED);
619             mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_MAX_REACHED);
620             mOverflowBubbles.remove(oldest);
621             mStateChange.removedOverflowBubble = oldest;
622         }
623     }
624 
dismissAll(@ismissReason int reason)625     public void dismissAll(@DismissReason int reason) {
626         if (DEBUG_BUBBLE_DATA) {
627             Log.d(TAG, "dismissAll: reason=" + reason);
628         }
629         if (mBubbles.isEmpty()) {
630             return;
631         }
632         setExpandedInternal(false);
633         setSelectedBubbleInternal(null);
634         while (!mBubbles.isEmpty()) {
635             doRemove(mBubbles.get(0).getKey(), reason);
636         }
637         dispatchPendingChanges();
638     }
639 
640     /**
641      * Called in response to the visibility of a locusId changing. A locusId is set on a task
642      * and if there's a matching bubble for that locusId then the bubble may be hidden or shown
643      * depending on the visibility of the locusId.
644      *
645      * @param taskId the taskId associated with the locusId visibility change.
646      * @param locusId the locusId whose visibility has changed.
647      * @param visible whether the task with the locusId is visible or not.
648      */
onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible)649     public void onLocusVisibilityChanged(int taskId, LocusId locusId, boolean visible) {
650         Bubble matchingBubble = getBubbleInStackWithLocusId(locusId);
651         // Don't add the locus if it's from a bubble'd activity, we only suppress for non-bubbled.
652         if (visible && (matchingBubble == null || matchingBubble.getTaskId() != taskId)) {
653             mVisibleLocusIds.add(locusId);
654         } else {
655             mVisibleLocusIds.remove(locusId);
656         }
657         if (matchingBubble == null) {
658             return;
659         }
660         boolean isAlreadySuppressed = mSuppressedBubbles.get(locusId) != null;
661         if (visible && !isAlreadySuppressed && matchingBubble.isSuppressable()
662                 && taskId != matchingBubble.getTaskId()) {
663             mSuppressedBubbles.put(locusId, matchingBubble);
664             matchingBubble.setSuppressBubble(true);
665             mStateChange.suppressedBubble = matchingBubble;
666             dispatchPendingChanges();
667         } else if (!visible) {
668             Bubble unsuppressedBubble = mSuppressedBubbles.remove(locusId);
669             if (unsuppressedBubble != null) {
670                 unsuppressedBubble.setSuppressBubble(false);
671                 mStateChange.unsuppressedBubble = unsuppressedBubble;
672             }
673             dispatchPendingChanges();
674         }
675     }
676 
677     /**
678      * Removes all bubbles from the overflow, called when the user changes.
679      */
clearOverflow()680     public void clearOverflow() {
681         while (!mOverflowBubbles.isEmpty()) {
682             doRemove(mOverflowBubbles.get(0).getKey(), Bubbles.DISMISS_USER_CHANGED);
683         }
684         dispatchPendingChanges();
685     }
686 
dispatchPendingChanges()687     private void dispatchPendingChanges() {
688         if (mListener != null && mStateChange.anythingChanged()) {
689             mListener.applyUpdate(mStateChange);
690         }
691         mStateChange = new Update(mBubbles, mOverflowBubbles);
692     }
693 
694     /**
695      * Requests a change to the selected bubble.
696      *
697      * @param bubble the new selected bubble
698      */
setSelectedBubbleInternal(@ullable BubbleViewProvider bubble)699     private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) {
700         if (DEBUG_BUBBLE_DATA) {
701             Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
702         }
703         if (Objects.equals(bubble, mSelectedBubble)) {
704             return;
705         }
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.getKey().equals(mOverflow.getKey()) && !mBubbles.isEmpty()) {
775                 // Show previously selected bubble instead of overflow menu when expanding.
776                 setSelectedBubbleInternal(mBubbles.get(0));
777             }
778             if (mSelectedBubble instanceof Bubble) {
779                 ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
780             }
781             mStateChange.orderChanged |= repackAll();
782         } else if (!mBubbles.isEmpty()) {
783             // Apply ordering and grouping rules from expanded -> collapsed, then save
784             // the result.
785             mStateChange.orderChanged |= repackAll();
786             if (mBubbles.indexOf(mSelectedBubble) > 0) {
787                 // Move the selected bubble to the top while collapsed.
788                 int index = mBubbles.indexOf(mSelectedBubble);
789                 if (index != 0) {
790                     mBubbles.remove((Bubble) mSelectedBubble);
791                     mBubbles.add(0, (Bubble) mSelectedBubble);
792                     mStateChange.orderChanged = true;
793                 }
794             }
795         }
796         if (mNeedsTrimming) {
797             mNeedsTrimming = false;
798             trim();
799         }
800         mExpanded = shouldExpand;
801         mStateChange.expanded = shouldExpand;
802         mStateChange.expandedChanged = true;
803     }
804 
sortKey(Bubble bubble)805     private static long sortKey(Bubble bubble) {
806         return bubble.getLastActivity();
807     }
808 
809     /**
810      * This applies a full sort and group pass to all existing bubbles.
811      * Bubbles are sorted by lastUpdated descending.
812      *
813      * @return true if the position of any bubbles changed as a result
814      */
repackAll()815     private boolean repackAll() {
816         if (DEBUG_BUBBLE_DATA) {
817             Log.d(TAG, "repackAll()");
818         }
819         if (mBubbles.isEmpty()) {
820             return false;
821         }
822         List<Bubble> repacked = new ArrayList<>(mBubbles.size());
823         // Add bubbles, freshest to oldest
824         mBubbles.stream()
825                 .sorted(BUBBLES_BY_SORT_KEY_DESCENDING)
826                 .forEachOrdered(repacked::add);
827         if (repacked.equals(mBubbles)) {
828             return false;
829         }
830         mBubbles.clear();
831         mBubbles.addAll(repacked);
832         return true;
833     }
834 
maybeSendDeleteIntent(@ismissReason int reason, @NonNull final Bubble bubble)835     private void maybeSendDeleteIntent(@DismissReason int reason, @NonNull final Bubble bubble) {
836         if (reason != Bubbles.DISMISS_USER_GESTURE) return;
837         PendingIntent deleteIntent = bubble.getDeleteIntent();
838         if (deleteIntent == null) return;
839         try {
840             deleteIntent.send();
841         } catch (PendingIntent.CanceledException e) {
842             Log.w(TAG, "Failed to send delete intent for bubble with key: " + bubble.getKey());
843         }
844     }
845 
indexForKey(String key)846     private int indexForKey(String key) {
847         for (int i = 0; i < mBubbles.size(); i++) {
848             Bubble bubble = mBubbles.get(i);
849             if (bubble.getKey().equals(key)) {
850                 return i;
851             }
852         }
853         return -1;
854     }
855 
856     /**
857      * The set of bubbles in row.
858      */
859     @VisibleForTesting(visibility = PACKAGE)
getBubbles()860     public List<Bubble> getBubbles() {
861         return Collections.unmodifiableList(mBubbles);
862     }
863 
864     /**
865      * The set of bubbles in overflow.
866      */
867     @VisibleForTesting(visibility = PRIVATE)
getOverflowBubbles()868     public List<Bubble> getOverflowBubbles() {
869         return Collections.unmodifiableList(mOverflowBubbles);
870     }
871 
872     @VisibleForTesting(visibility = PRIVATE)
873     @Nullable
getAnyBubbleWithkey(String key)874     Bubble getAnyBubbleWithkey(String key) {
875         Bubble b = getBubbleInStackWithKey(key);
876         if (b == null) {
877             b = getOverflowBubbleWithKey(key);
878         }
879         return b;
880     }
881 
882     /** @return any bubble (in the stack or the overflow) that matches the provided shortcutId. */
883     @Nullable
getAnyBubbleWithShortcutId(String shortcutId)884     Bubble getAnyBubbleWithShortcutId(String shortcutId) {
885         if (TextUtils.isEmpty(shortcutId)) {
886             return null;
887         }
888         for (int i = 0; i < mBubbles.size(); i++) {
889             Bubble bubble = mBubbles.get(i);
890             String bubbleShortcutId = bubble.getShortcutInfo() != null
891                     ? bubble.getShortcutInfo().getId()
892                     : bubble.getMetadataShortcutId();
893             if (shortcutId.equals(bubbleShortcutId)) {
894                 return bubble;
895             }
896         }
897 
898         for (int i = 0; i < mOverflowBubbles.size(); i++) {
899             Bubble bubble = mOverflowBubbles.get(i);
900             String bubbleShortcutId = bubble.getShortcutInfo() != null
901                     ? bubble.getShortcutInfo().getId()
902                     : bubble.getMetadataShortcutId();
903             if (shortcutId.equals(bubbleShortcutId)) {
904                 return bubble;
905             }
906         }
907         return null;
908     }
909 
910     @VisibleForTesting(visibility = PRIVATE)
911     @Nullable
getBubbleInStackWithKey(String key)912     public Bubble getBubbleInStackWithKey(String key) {
913         for (int i = 0; i < mBubbles.size(); i++) {
914             Bubble bubble = mBubbles.get(i);
915             if (bubble.getKey().equals(key)) {
916                 return bubble;
917             }
918         }
919         return null;
920     }
921 
922     @Nullable
getBubbleInStackWithLocusId(LocusId locusId)923     private Bubble getBubbleInStackWithLocusId(LocusId locusId) {
924         if (locusId == null) return null;
925         for (int i = 0; i < mBubbles.size(); i++) {
926             Bubble bubble = mBubbles.get(i);
927             if (locusId.equals(bubble.getLocusId())) {
928                 return bubble;
929             }
930         }
931         return null;
932     }
933 
934     @Nullable
getBubbleWithView(View view)935     Bubble getBubbleWithView(View view) {
936         for (int i = 0; i < mBubbles.size(); i++) {
937             Bubble bubble = mBubbles.get(i);
938             if (bubble.getIconView() != null && bubble.getIconView().equals(view)) {
939                 return bubble;
940             }
941         }
942         return null;
943     }
944 
945     @VisibleForTesting(visibility = PRIVATE)
getOverflowBubbleWithKey(String key)946     public Bubble getOverflowBubbleWithKey(String key) {
947         for (int i = 0; i < mOverflowBubbles.size(); i++) {
948             Bubble bubble = mOverflowBubbles.get(i);
949             if (bubble.getKey().equals(key)) {
950                 return bubble;
951             }
952         }
953         return null;
954     }
955 
956     @VisibleForTesting(visibility = PRIVATE)
setTimeSource(TimeSource timeSource)957     void setTimeSource(TimeSource timeSource) {
958         mTimeSource = timeSource;
959     }
960 
setListener(Listener listener)961     public void setListener(Listener listener) {
962         mListener = listener;
963     }
964 
965     /**
966      * Set maximum number of bubbles allowed in overflow.
967      * This method should only be used in tests, not in production.
968      */
969     @VisibleForTesting
setMaxOverflowBubbles(int maxOverflowBubbles)970     public void setMaxOverflowBubbles(int maxOverflowBubbles) {
971         mMaxOverflowBubbles = maxOverflowBubbles;
972     }
973 
974     /**
975      * Description of current bubble data state.
976      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)977     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
978         pw.print("selected: ");
979         pw.println(mSelectedBubble != null
980                 ? mSelectedBubble.getKey()
981                 : "null");
982         pw.print("expanded: ");
983         pw.println(mExpanded);
984 
985         pw.print("stack bubble count:    ");
986         pw.println(mBubbles.size());
987         for (Bubble bubble : mBubbles) {
988             bubble.dump(fd, pw, args);
989         }
990 
991         pw.print("overflow bubble count:    ");
992         pw.println(mOverflowBubbles.size());
993         for (Bubble bubble : mOverflowBubbles) {
994             bubble.dump(fd, pw, args);
995         }
996 
997         pw.print("summaryKeys: ");
998         pw.println(mSuppressedGroupKeys.size());
999         for (String key : mSuppressedGroupKeys.keySet()) {
1000             pw.println("   suppressing: " + key);
1001         }
1002     }
1003 }
1004