1 /*
2  * Copyright (C) 2018 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 
17 package com.android.systemui.bubbles;
18 
19 import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
20 import static android.app.Notification.FLAG_BUBBLE;
21 import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
22 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
23 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
24 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
25 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
26 import static android.service.notification.NotificationListenerService.REASON_CLICK;
27 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
28 import static android.view.Display.DEFAULT_DISPLAY;
29 import static android.view.Display.INVALID_DISPLAY;
30 import static android.view.View.INVISIBLE;
31 import static android.view.View.VISIBLE;
32 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
33 
34 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
35 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
36 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
37 import static com.android.systemui.statusbar.StatusBarState.SHADE;
38 import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON;
39 
40 import static java.lang.annotation.ElementType.FIELD;
41 import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
42 import static java.lang.annotation.ElementType.PARAMETER;
43 import static java.lang.annotation.RetentionPolicy.SOURCE;
44 
45 import android.annotation.UserIdInt;
46 import android.app.ActivityManager.RunningTaskInfo;
47 import android.app.NotificationManager;
48 import android.app.PendingIntent;
49 import android.content.Context;
50 import android.content.pm.ActivityInfo;
51 import android.content.pm.ParceledListSlice;
52 import android.content.res.Configuration;
53 import android.graphics.Rect;
54 import android.os.RemoteException;
55 import android.os.ServiceManager;
56 import android.provider.Settings;
57 import android.service.notification.NotificationListenerService.RankingMap;
58 import android.service.notification.ZenModeConfig;
59 import android.util.ArraySet;
60 import android.util.Log;
61 import android.util.Pair;
62 import android.util.SparseSetArray;
63 import android.view.Display;
64 import android.view.IPinnedStackController;
65 import android.view.IPinnedStackListener;
66 import android.view.ViewGroup;
67 import android.widget.FrameLayout;
68 
69 import androidx.annotation.IntDef;
70 import androidx.annotation.MainThread;
71 import androidx.annotation.Nullable;
72 
73 import com.android.internal.annotations.VisibleForTesting;
74 import com.android.internal.statusbar.IStatusBarService;
75 import com.android.systemui.Dependency;
76 import com.android.systemui.R;
77 import com.android.systemui.plugins.statusbar.StatusBarStateController;
78 import com.android.systemui.shared.system.ActivityManagerWrapper;
79 import com.android.systemui.shared.system.TaskStackChangeListener;
80 import com.android.systemui.shared.system.WindowManagerWrapper;
81 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
82 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
83 import com.android.systemui.statusbar.notification.NotificationEntryListener;
84 import com.android.systemui.statusbar.notification.NotificationEntryManager;
85 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
86 import com.android.systemui.statusbar.notification.collection.NotificationData;
87 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
88 import com.android.systemui.statusbar.phone.NotificationGroupManager;
89 import com.android.systemui.statusbar.phone.StatusBarWindowController;
90 import com.android.systemui.statusbar.policy.ConfigurationController;
91 import com.android.systemui.statusbar.policy.ZenModeController;
92 
93 import java.io.FileDescriptor;
94 import java.io.PrintWriter;
95 import java.lang.annotation.Retention;
96 import java.lang.annotation.Target;
97 import java.util.ArrayList;
98 import java.util.List;
99 
100 import javax.inject.Inject;
101 import javax.inject.Singleton;
102 
103 /**
104  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
105  * Bubbles can be expanded to show more content.
106  *
107  * The controller manages addition, removal, and visible state of bubbles on screen.
108  */
109 @Singleton
110 public class BubbleController implements ConfigurationController.ConfigurationListener {
111 
112     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
113 
114     @Retention(SOURCE)
115     @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
116             DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
117             DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT})
118     @Target({FIELD, LOCAL_VARIABLE, PARAMETER})
119     @interface DismissReason {}
120 
121     static final int DISMISS_USER_GESTURE = 1;
122     static final int DISMISS_AGED = 2;
123     static final int DISMISS_TASK_FINISHED = 3;
124     static final int DISMISS_BLOCKED = 4;
125     static final int DISMISS_NOTIF_CANCEL = 5;
126     static final int DISMISS_ACCESSIBILITY_ACTION = 6;
127     static final int DISMISS_NO_LONGER_BUBBLE = 7;
128     static final int DISMISS_USER_CHANGED = 8;
129     static final int DISMISS_GROUP_CANCELLED = 9;
130     static final int DISMISS_INVALID_INTENT = 10;
131 
132     public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
133 
134     /** Flag to enable or disable the entire feature */
135     private static final String ENABLE_BUBBLES = "experiment_enable_bubbles";
136 
137     private final Context mContext;
138     private final NotificationEntryManager mNotificationEntryManager;
139     private final BubbleTaskStackListener mTaskStackListener;
140     private BubbleStateChangeListener mStateChangeListener;
141     private BubbleExpandListener mExpandListener;
142     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
143     private final NotificationGroupManager mNotificationGroupManager;
144 
145     private BubbleData mBubbleData;
146     @Nullable private BubbleStackView mStackView;
147 
148     // Tracks the id of the current (foreground) user.
149     private int mCurrentUserId;
150     // Saves notification keys of active bubbles when users are switched.
151     private final SparseSetArray<String> mSavedBubbleKeysPerUser;
152 
153     // Bubbles get added to the status bar view
154     private final StatusBarWindowController mStatusBarWindowController;
155     private final ZenModeController mZenModeController;
156     private StatusBarStateListener mStatusBarStateListener;
157 
158     private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
159     private IStatusBarService mBarService;
160 
161     // Used for determining view rect for touch interaction
162     private Rect mTempRect = new Rect();
163 
164     // Listens to user switch so bubbles can be saved and restored.
165     private final NotificationLockscreenUserManager mNotifUserManager;
166 
167     /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
168     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
169 
170     /**
171      * Listener to be notified when some states of the bubbles change.
172      */
173     public interface BubbleStateChangeListener {
174         /**
175          * Called when the stack has bubbles or no longer has bubbles.
176          */
onHasBubblesChanged(boolean hasBubbles)177         void onHasBubblesChanged(boolean hasBubbles);
178     }
179 
180     /**
181      * Listener to find out about stack expansion / collapse events.
182      */
183     public interface BubbleExpandListener {
184         /**
185          * Called when the expansion state of the bubble stack changes.
186          *
187          * @param isExpanding whether it's expanding or collapsing
188          * @param key the notification key associated with bubble being expanded
189          */
onBubbleExpandChanged(boolean isExpanding, String key)190         void onBubbleExpandChanged(boolean isExpanding, String key);
191     }
192 
193     /**
194      * Listens for the current state of the status bar and updates the visibility state
195      * of bubbles as needed.
196      */
197     private class StatusBarStateListener implements StatusBarStateController.StateListener {
198         private int mState;
199         /**
200          * Returns the current status bar state.
201          */
getCurrentState()202         public int getCurrentState() {
203             return mState;
204         }
205 
206         @Override
onStateChanged(int newState)207         public void onStateChanged(int newState) {
208             mState = newState;
209             boolean shouldCollapse = (mState != SHADE);
210             if (shouldCollapse) {
211                 collapseStack();
212             }
213             updateStack();
214         }
215     }
216 
217     @Inject
BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager)218     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
219             BubbleData data, ConfigurationController configurationController,
220             NotificationInterruptionStateProvider interruptionStateProvider,
221             ZenModeController zenModeController,
222             NotificationLockscreenUserManager notifUserManager,
223             NotificationGroupManager groupManager) {
224         this(context, statusBarWindowController, data, null /* synchronizer */,
225                 configurationController, interruptionStateProvider, zenModeController,
226                 notifUserManager, groupManager);
227     }
228 
BubbleController(Context context, StatusBarWindowController statusBarWindowController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptionStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager)229     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
230             BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
231             ConfigurationController configurationController,
232             NotificationInterruptionStateProvider interruptionStateProvider,
233             ZenModeController zenModeController,
234             NotificationLockscreenUserManager notifUserManager,
235             NotificationGroupManager groupManager) {
236         mContext = context;
237         mNotificationInterruptionStateProvider = interruptionStateProvider;
238         mNotifUserManager = notifUserManager;
239         mZenModeController = zenModeController;
240         mZenModeController.addCallback(new ZenModeController.Callback() {
241             @Override
242             public void onZenChanged(int zen) {
243                 if (mStackView != null) {
244                     mStackView.updateDots();
245                 }
246             }
247 
248             @Override
249             public void onConfigChanged(ZenModeConfig config) {
250                 if (mStackView != null) {
251                     mStackView.updateDots();
252                 }
253             }
254         });
255 
256         configurationController.addCallback(this /* configurationListener */);
257 
258         mBubbleData = data;
259         mBubbleData.setListener(mBubbleDataListener);
260 
261         mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
262         mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
263         mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
264         mNotificationGroupManager = groupManager;
265         mNotificationGroupManager.addOnGroupChangeListener(
266                 new NotificationGroupManager.OnGroupChangeListener() {
267                     @Override
268                     public void onGroupSuppressionChanged(
269                             NotificationGroupManager.NotificationGroup group,
270                             boolean suppressed) {
271                         // More notifications could be added causing summary to no longer
272                         // be suppressed -- in this case need to remove the key.
273                         final String groupKey = group.summary != null
274                                 ? group.summary.notification.getGroupKey()
275                                 : null;
276                         if (!suppressed && groupKey != null
277                                 && mBubbleData.isSummarySuppressed(groupKey)) {
278                             mBubbleData.removeSuppressedSummary(groupKey);
279                         }
280                     }
281                 });
282 
283         mStatusBarWindowController = statusBarWindowController;
284         mStatusBarStateListener = new StatusBarStateListener();
285         Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
286 
287         mTaskStackListener = new BubbleTaskStackListener();
288         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
289 
290         try {
291             WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener());
292         } catch (RemoteException e) {
293             e.printStackTrace();
294         }
295         mSurfaceSynchronizer = synchronizer;
296 
297         mBarService = IStatusBarService.Stub.asInterface(
298                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
299 
300         mSavedBubbleKeysPerUser = new SparseSetArray<>();
301         mCurrentUserId = mNotifUserManager.getCurrentUserId();
302         mNotifUserManager.addUserChangedListener(
303                 newUserId -> {
304                     saveBubbles(mCurrentUserId);
305                     mBubbleData.dismissAll(DISMISS_USER_CHANGED);
306                     restoreBubbles(newUserId);
307                     mCurrentUserId = newUserId;
308                 });
309     }
310 
311     /**
312      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
313      * method initializes the stack view and adds it to the StatusBar just above the scrim.
314      */
ensureStackViewCreated()315     private void ensureStackViewCreated() {
316         if (mStackView == null) {
317             mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer);
318             ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
319             // TODO(b/130237686): When you expand the shade on top of expanded bubble, there is no
320             //  scrim between bubble and the shade
321             int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
322             sbv.addView(mStackView, bubblePosition,
323                     new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
324             if (mExpandListener != null) {
325                 mStackView.setExpandListener(mExpandListener);
326             }
327         }
328     }
329 
330     /**
331      * Records the notification key for any active bubbles. These are used to restore active
332      * bubbles when the user returns to the foreground.
333      *
334      * @param userId the id of the user
335      */
saveBubbles(@serIdInt int userId)336     private void saveBubbles(@UserIdInt int userId) {
337         // First clear any existing keys that might be stored.
338         mSavedBubbleKeysPerUser.remove(userId);
339         // Add in all active bubbles for the current user.
340         for (Bubble bubble: mBubbleData.getBubbles()) {
341             mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
342         }
343     }
344 
345     /**
346      * Promotes existing notifications to Bubbles if they were previously bubbles.
347      *
348      * @param userId the id of the user
349      */
restoreBubbles(@serIdInt int userId)350     private void restoreBubbles(@UserIdInt int userId) {
351         NotificationData notificationData =
352                 mNotificationEntryManager.getNotificationData();
353         ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId);
354         if (savedBubbleKeys == null) {
355             // There were no bubbles saved for this used.
356             return;
357         }
358         for (NotificationEntry e : notificationData.getNotificationsForCurrentUser()) {
359             if (savedBubbleKeys.contains(e.key)
360                     && mNotificationInterruptionStateProvider.shouldBubbleUp(e)
361                     && canLaunchInActivityView(mContext, e)) {
362                 updateBubble(e, /* suppressFlyout= */ true);
363             }
364         }
365         // Finally, remove the entries for this user now that bubbles are restored.
366         mSavedBubbleKeysPerUser.remove(mCurrentUserId);
367     }
368 
369     @Override
onUiModeChanged()370     public void onUiModeChanged() {
371         if (mStackView != null) {
372             mStackView.onThemeChanged();
373         }
374     }
375 
376     @Override
onOverlayChanged()377     public void onOverlayChanged() {
378         if (mStackView != null) {
379             mStackView.onThemeChanged();
380         }
381     }
382 
383     @Override
onConfigChanged(Configuration newConfig)384     public void onConfigChanged(Configuration newConfig) {
385         if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) {
386             mOrientation = newConfig.orientation;
387             mStackView.onOrientationChanged(newConfig.orientation);
388         }
389     }
390 
391     /**
392      * Set a listener to be notified when some states of the bubbles change.
393      */
setBubbleStateChangeListener(BubbleStateChangeListener listener)394     public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
395         mStateChangeListener = listener;
396     }
397 
398     /**
399      * Set a listener to be notified of bubble expand events.
400      */
setExpandListener(BubbleExpandListener listener)401     public void setExpandListener(BubbleExpandListener listener) {
402         mExpandListener = ((isExpanding, key) -> {
403             if (listener != null) {
404                 listener.onBubbleExpandChanged(isExpanding, key);
405             }
406             mStatusBarWindowController.setBubbleExpanded(isExpanding);
407         });
408         if (mStackView != null) {
409             mStackView.setExpandListener(mExpandListener);
410         }
411     }
412 
413     /**
414      * Whether or not there are bubbles present, regardless of them being visible on the
415      * screen (e.g. if on AOD).
416      */
hasBubbles()417     public boolean hasBubbles() {
418         if (mStackView == null) {
419             return false;
420         }
421         return mBubbleData.hasBubbles();
422     }
423 
424     /**
425      * Whether the stack of bubbles is expanded or not.
426      */
isStackExpanded()427     public boolean isStackExpanded() {
428         return mBubbleData.isExpanded();
429     }
430 
431     /**
432      * Tell the stack of bubbles to expand.
433      */
expandStack()434     public void expandStack() {
435         mBubbleData.setExpanded(true);
436     }
437 
438     /**
439      * Tell the stack of bubbles to collapse.
440      */
collapseStack()441     public void collapseStack() {
442         mBubbleData.setExpanded(false /* expanded */);
443     }
444 
445     /**
446      * True if either:
447      * (1) There is a bubble associated with the provided key and if its notification is hidden
448      *     from the shade.
449      * (2) There is a group summary associated with the provided key that is hidden from the shade
450      *     because it has been dismissed but still has child bubbles active.
451      *
452      * False otherwise.
453      */
isBubbleNotificationSuppressedFromShade(String key)454     public boolean isBubbleNotificationSuppressedFromShade(String key) {
455         boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key)
456                 && !mBubbleData.getBubbleWithKey(key).showInShadeWhenBubble();
457         NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key);
458         String groupKey = entry != null ? entry.notification.getGroupKey() : null;
459         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
460         boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey));
461         return (isSummary && isSuppressedSummary) || isBubbleAndSuppressed;
462     }
463 
selectBubble(Bubble bubble)464     void selectBubble(Bubble bubble) {
465         mBubbleData.setSelectedBubble(bubble);
466     }
467 
468     @VisibleForTesting
selectBubble(String key)469     void selectBubble(String key) {
470         Bubble bubble = mBubbleData.getBubbleWithKey(key);
471         selectBubble(bubble);
472     }
473 
474     /**
475      * Request the stack expand if needed, then select the specified Bubble as current.
476      *
477      * @param notificationKey the notification key for the bubble to be selected
478      */
expandStackAndSelectBubble(String notificationKey)479     public void expandStackAndSelectBubble(String notificationKey) {
480         Bubble bubble = mBubbleData.getBubbleWithKey(notificationKey);
481         if (bubble != null) {
482             mBubbleData.setSelectedBubble(bubble);
483             mBubbleData.setExpanded(true);
484         }
485     }
486 
487     /**
488      * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
489      */
dismissStack(@ismissReason int reason)490     void dismissStack(@DismissReason int reason) {
491         mBubbleData.dismissAll(reason);
492     }
493 
494     /**
495      * Directs a back gesture at the bubble stack. When opened, the current expanded bubble
496      * is forwarded a back key down/up pair.
497      */
performBackPressIfNeeded()498     public void performBackPressIfNeeded() {
499         if (mStackView != null) {
500             mStackView.performBackPressIfNeeded();
501         }
502     }
503 
504     /**
505      * Adds or updates a bubble associated with the provided notification entry.
506      *
507      * @param notif the notification associated with this bubble.
508      */
updateBubble(NotificationEntry notif)509     void updateBubble(NotificationEntry notif) {
510         updateBubble(notif, /* supressFlyout */ false);
511     }
512 
updateBubble(NotificationEntry notif, boolean suppressFlyout)513     void updateBubble(NotificationEntry notif, boolean suppressFlyout) {
514         // If this is an interruptive notif, mark that it's interrupted
515         if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) {
516             notif.setInterruption();
517         }
518         mBubbleData.notificationEntryUpdated(notif, suppressFlyout);
519     }
520 
521     /**
522      * Removes the bubble associated with the {@param uri}.
523      * <p>
524      * Must be called from the main thread.
525      */
526     @MainThread
removeBubble(String key, int reason)527     void removeBubble(String key, int reason) {
528         // TEMP: refactor to change this to pass entry
529         Bubble bubble = mBubbleData.getBubbleWithKey(key);
530         if (bubble != null) {
531             mBubbleData.notificationEntryRemoved(bubble.getEntry(), reason);
532         }
533     }
534 
535     @SuppressWarnings("FieldCanBeLocal")
536     private final NotificationRemoveInterceptor mRemoveInterceptor =
537             new NotificationRemoveInterceptor() {
538             @Override
539             public boolean onNotificationRemoveRequested(String key, int reason) {
540                 NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key);
541                 String groupKey = entry != null ? entry.notification.getGroupKey() : null;
542                 ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
543 
544                 boolean inBubbleData = mBubbleData.hasBubbleWithKey(key);
545                 boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey)
546                         && mBubbleData.getSummaryKey(groupKey).equals(key));
547                 boolean isSummary = entry != null
548                         && entry.notification.getNotification().isGroupSummary();
549                 boolean isSummaryOfBubbles = (isSuppressedSummary || isSummary)
550                         && bubbleChildren != null && !bubbleChildren.isEmpty();
551 
552                 if (!inBubbleData && !isSummaryOfBubbles) {
553                     return false;
554                 }
555 
556                 final boolean isClearAll = reason == REASON_CANCEL_ALL;
557                 final boolean isUserDimiss = reason == REASON_CANCEL || reason == REASON_CLICK;
558                 final boolean isAppCancel = reason == REASON_APP_CANCEL
559                         || reason == REASON_APP_CANCEL_ALL;
560                 final boolean isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED;
561 
562                 // Need to check for !appCancel here because the notification may have
563                 // previously been dismissed & entry.isRowDismissed would still be true
564                 boolean userRemovedNotif = (entry != null && entry.isRowDismissed() && !isAppCancel)
565                         || isClearAll || isUserDimiss || isSummaryCancel;
566 
567                 if (isSummaryOfBubbles) {
568                     return handleSummaryRemovalInterception(entry, userRemovedNotif);
569                 }
570 
571                 // The bubble notification sticks around in the data as long as the bubble is
572                 // not dismissed and the app hasn't cancelled the notification.
573                 Bubble bubble = mBubbleData.getBubbleWithKey(key);
574                 boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
575                 if (bubbleExtended) {
576                     bubble.setShowInShadeWhenBubble(false);
577                     bubble.setShowBubbleDot(false);
578                     if (mStackView != null) {
579                         mStackView.updateDotVisibility(entry.key);
580                     }
581                     mNotificationEntryManager.updateNotifications();
582                     return true;
583                 } else if (!userRemovedNotif && entry != null) {
584                     // This wasn't a user removal so we should remove the bubble as well
585                     mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
586                     return false;
587                 }
588                 return false;
589             }
590         };
591 
handleSummaryRemovalInterception(NotificationEntry summary, boolean userRemovedNotif)592     private boolean handleSummaryRemovalInterception(NotificationEntry summary,
593             boolean userRemovedNotif) {
594         String groupKey = summary.notification.getGroupKey();
595         ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(groupKey);
596 
597         if (userRemovedNotif) {
598             // If it's a user dismiss we mark the children to be hidden from the shade.
599             for (int i = 0; i < bubbleChildren.size(); i++) {
600                 Bubble bubbleChild = bubbleChildren.get(i);
601                 // As far as group manager is concerned, once a child is no longer shown
602                 // in the shade, it is essentially removed.
603                 mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry());
604                 bubbleChild.setShowInShadeWhenBubble(false);
605                 bubbleChild.setShowBubbleDot(false);
606                 if (mStackView != null) {
607                     mStackView.updateDotVisibility(bubbleChild.getKey());
608                 }
609             }
610             // And since all children are removed, remove the summary.
611             mNotificationGroupManager.onEntryRemoved(summary);
612 
613             // If the summary was auto-generated we don't need to keep that notification around
614             // because apps can't cancel it; so we only intercept & suppress real summaries.
615             boolean isAutogroupSummary = (summary.notification.getNotification().flags
616                     & FLAG_AUTOGROUP_SUMMARY) != 0;
617             if (!isAutogroupSummary) {
618                 mBubbleData.addSummaryToSuppress(summary.notification.getGroupKey(),
619                         summary.key);
620                 // Tell shade to update for the suppression
621                 mNotificationEntryManager.updateNotifications();
622             }
623             return !isAutogroupSummary;
624         } else {
625             // If it's not a user dismiss it's a cancel.
626             mBubbleData.removeSuppressedSummary(groupKey);
627 
628             // Remove any associated bubble children.
629             for (int i = 0; i < bubbleChildren.size(); i++) {
630                 Bubble bubbleChild = bubbleChildren.get(i);
631                 mBubbleData.notificationEntryRemoved(bubbleChild.getEntry(),
632                         DISMISS_GROUP_CANCELLED);
633             }
634             return false;
635         }
636     }
637 
638     @SuppressWarnings("FieldCanBeLocal")
639     private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
640         @Override
641         public void onPendingEntryAdded(NotificationEntry entry) {
642             if (!areBubblesEnabled(mContext)) {
643                 return;
644             }
645             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
646                     && canLaunchInActivityView(mContext, entry)) {
647                 updateBubble(entry);
648             }
649         }
650 
651         @Override
652         public void onPreEntryUpdated(NotificationEntry entry) {
653             if (!areBubblesEnabled(mContext)) {
654                 return;
655             }
656             boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
657                     && canLaunchInActivityView(mContext, entry);
658             if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.key)) {
659                 // It was previously a bubble but no longer a bubble -- lets remove it
660                 removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE);
661             } else if (shouldBubble) {
662                 Bubble b = mBubbleData.getBubbleWithKey(entry.key);
663                 updateBubble(entry);
664             }
665         }
666 
667         @Override
668         public void onNotificationRankingUpdated(RankingMap rankingMap) {
669             // Forward to BubbleData to block any bubbles which should no longer be shown
670             mBubbleData.notificationRankingUpdated(rankingMap);
671         }
672     };
673 
674     @SuppressWarnings("FieldCanBeLocal")
675     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
676 
677         @Override
678         public void applyUpdate(BubbleData.Update update) {
679             if (mStackView == null && update.addedBubble != null) {
680                 // Lazy init stack view when the first bubble is added.
681                 ensureStackViewCreated();
682             }
683 
684             // If not yet initialized, ignore all other changes.
685             if (mStackView == null) {
686                 return;
687             }
688 
689             if (update.addedBubble != null) {
690                 mStackView.addBubble(update.addedBubble);
691             }
692 
693             // Collapsing? Do this first before remaining steps.
694             if (update.expandedChanged && !update.expanded) {
695                 mStackView.setExpanded(false);
696             }
697 
698             // Do removals, if any.
699             ArrayList<Pair<Bubble, Integer>> removedBubbles =
700                     new ArrayList<>(update.removedBubbles);
701             for (Pair<Bubble, Integer> removed : removedBubbles) {
702                 final Bubble bubble = removed.first;
703                 @DismissReason final int reason = removed.second;
704                 mStackView.removeBubble(bubble);
705 
706                 // If the bubble is removed for user switching, leave the notification in place.
707                 if (reason != DISMISS_USER_CHANGED) {
708                     if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
709                             && !bubble.showInShadeWhenBubble()) {
710                         // The bubble is gone & the notification is gone, time to actually remove it
711                         mNotificationEntryManager.performRemoveNotification(
712                                 bubble.getEntry().notification, UNDEFINED_DISMISS_REASON);
713                     } else {
714                         // Update the flag for SysUI
715                         bubble.getEntry().notification.getNotification().flags &= ~FLAG_BUBBLE;
716 
717                         // Make sure NoMan knows it's not a bubble anymore so anyone querying it
718                         // will get right result back
719                         try {
720                             mBarService.onNotificationBubbleChanged(bubble.getKey(),
721                                     false /* isBubble */);
722                         } catch (RemoteException e) {
723                             // Bad things have happened
724                         }
725                     }
726 
727                     // Check if removed bubble has an associated suppressed group summary that needs
728                     // to be removed now.
729                     final String groupKey = bubble.getEntry().notification.getGroupKey();
730                     if (mBubbleData.isSummarySuppressed(groupKey)
731                             && mBubbleData.getBubblesInGroup(groupKey).isEmpty()) {
732                         // Time to actually remove the summary.
733                         String notifKey = mBubbleData.getSummaryKey(groupKey);
734                         mBubbleData.removeSuppressedSummary(groupKey);
735                         NotificationEntry entry =
736                                 mNotificationEntryManager.getNotificationData().get(notifKey);
737                         mNotificationEntryManager.performRemoveNotification(
738                                 entry.notification, UNDEFINED_DISMISS_REASON);
739                     }
740 
741                     // Check if summary should be removed from NoManGroup
742                     NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary(
743                             bubble.getEntry().notification);
744                     if (summary != null) {
745                         ArrayList<NotificationEntry> summaryChildren =
746                                 mNotificationGroupManager.getLogicalChildren(summary.notification);
747                         boolean isSummaryThisNotif = summary.key.equals(bubble.getEntry().key);
748                         if (!isSummaryThisNotif
749                                 && (summaryChildren == null || summaryChildren.isEmpty())) {
750                             mNotificationEntryManager.performRemoveNotification(
751                                     summary.notification, UNDEFINED_DISMISS_REASON);
752                         }
753                     }
754                 }
755             }
756 
757             if (update.updatedBubble != null) {
758                 mStackView.updateBubble(update.updatedBubble);
759             }
760 
761             if (update.orderChanged) {
762                 mStackView.updateBubbleOrder(update.bubbles);
763             }
764 
765             if (update.selectionChanged) {
766                 mStackView.setSelectedBubble(update.selectedBubble);
767                 if (update.selectedBubble != null) {
768                     mNotificationGroupManager.updateSuppression(
769                             update.selectedBubble.getEntry());
770                 }
771             }
772 
773             // Expanding? Apply this last.
774             if (update.expandedChanged && update.expanded) {
775                 mStackView.setExpanded(true);
776             }
777 
778             mNotificationEntryManager.updateNotifications();
779             updateStack();
780 
781             if (DEBUG_BUBBLE_CONTROLLER) {
782                 Log.d(TAG, "[BubbleData]");
783                 Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(),
784                         mBubbleData.getSelectedBubble()));
785 
786                 if (mStackView != null) {
787                     Log.d(TAG, "[BubbleStackView]");
788                     Log.d(TAG, formatBubblesString(mStackView.getBubblesOnScreen(),
789                             mStackView.getExpandedBubble()));
790                 }
791             }
792         }
793     };
794 
795     /**
796      * Lets any listeners know if bubble state has changed.
797      * Updates the visibility of the bubbles based on current state.
798      * Does not un-bubble, just hides or un-hides. Notifies any
799      * {@link BubbleStateChangeListener}s of visibility changes.
800      * Updates stack description for TalkBack focus.
801      */
updateStack()802     public void updateStack() {
803         if (mStackView == null) {
804             return;
805         }
806         if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) {
807             // Bubbles only appear in unlocked shade
808             mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE);
809         } else if (mStackView != null) {
810             mStackView.setVisibility(INVISIBLE);
811         }
812 
813         // Let listeners know if bubble state changed.
814         boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
815         boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE;
816         mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
817         if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
818             mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
819         }
820 
821         mStackView.updateContentDescription();
822     }
823 
824     /**
825      * Rect indicating the touchable region for the bubble stack / expanded stack.
826      */
getTouchableRegion()827     public Rect getTouchableRegion() {
828         if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
829             return null;
830         }
831         mStackView.getBoundsOnScreen(mTempRect);
832         return mTempRect;
833     }
834 
835     /**
836      * The display id of the expanded view, if the stack is expanded and not occluded by the
837      * status bar, otherwise returns {@link Display#INVALID_DISPLAY}.
838      */
getExpandedDisplayId(Context context)839     public int getExpandedDisplayId(Context context) {
840         final Bubble bubble = getExpandedBubble(context);
841         return bubble != null ? bubble.getDisplayId() : INVALID_DISPLAY;
842     }
843 
844     @Nullable
getExpandedBubble(Context context)845     private Bubble getExpandedBubble(Context context) {
846         if (mStackView == null) {
847             return null;
848         }
849         final boolean defaultDisplay = context.getDisplay() != null
850                 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY;
851         final Bubble expandedBubble = mStackView.getExpandedBubble();
852         if (defaultDisplay && expandedBubble != null && isStackExpanded()
853                 && !mStatusBarWindowController.getPanelExpanded()) {
854             return expandedBubble;
855         }
856         return null;
857     }
858 
859     @VisibleForTesting
getStackView()860     BubbleStackView getStackView() {
861         return mStackView;
862     }
863 
864     /**
865      * Description of current bubble state.
866      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)867     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
868         pw.println("BubbleController state:");
869         mBubbleData.dump(fd, pw, args);
870         pw.println();
871         if (mStackView != null) {
872             mStackView.dump(fd, pw, args);
873         }
874         pw.println();
875     }
876 
formatBubblesString(List<Bubble> bubbles, Bubble selected)877     static String formatBubblesString(List<Bubble> bubbles, Bubble selected) {
878         StringBuilder sb = new StringBuilder();
879         for (Bubble bubble : bubbles) {
880             if (bubble == null) {
881                 sb.append("   <null> !!!!!\n");
882             } else {
883                 boolean isSelected = (bubble == selected);
884                 sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n",
885                         ((isSelected) ? "->" : "  "),
886                         bubble.getLastActivity(),
887                         (bubble.isOngoing() ? 1 : 0),
888                         bubble.getKey()));
889             }
890         }
891         return sb.toString();
892     }
893 
894     /**
895      * This task stack listener is responsible for responding to tasks moved to the front
896      * which are on the default (main) display. When this happens, expanded bubbles must be
897      * collapsed so the user may interact with the app which was just moved to the front.
898      * <p>
899      * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches
900      * these calls via a main thread Handler.
901      */
902     @MainThread
903     private class BubbleTaskStackListener extends TaskStackChangeListener {
904 
905         @Override
onTaskMovedToFront(RunningTaskInfo taskInfo)906         public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
907             if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) {
908                 if (!mStackView.isExpansionAnimating()) {
909                     mBubbleData.setExpanded(false);
910                 }
911             }
912         }
913 
914         @Override
onActivityLaunchOnSecondaryDisplayRerouted()915         public void onActivityLaunchOnSecondaryDisplayRerouted() {
916             if (mStackView != null) {
917                 mBubbleData.setExpanded(false);
918             }
919         }
920 
921         @Override
onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)922         public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {
923             if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) {
924                 mBubbleData.setExpanded(false);
925             }
926         }
927 
928         @Override
onSingleTaskDisplayDrawn(int displayId)929         public void onSingleTaskDisplayDrawn(int displayId) {
930             final Bubble expandedBubble = mStackView != null
931                     ? mStackView.getExpandedBubble()
932                     : null;
933             if (expandedBubble != null && expandedBubble.getDisplayId() == displayId) {
934                 expandedBubble.setContentVisibility(true);
935             }
936         }
937 
938         @Override
onSingleTaskDisplayEmpty(int displayId)939         public void onSingleTaskDisplayEmpty(int displayId) {
940             final Bubble expandedBubble = mStackView != null
941                     ? mStackView.getExpandedBubble()
942                     : null;
943             int expandedId = expandedBubble != null ? expandedBubble.getDisplayId() : -1;
944             if (mStackView != null && mStackView.isExpanded() && expandedId == displayId) {
945                 mBubbleData.setExpanded(false);
946             }
947             mBubbleData.notifyDisplayEmpty(displayId);
948         }
949     }
950 
areBubblesEnabled(Context context)951     private static boolean areBubblesEnabled(Context context) {
952         return Settings.Secure.getInt(context.getContentResolver(),
953                 ENABLE_BUBBLES, 1) != 0;
954     }
955 
956     /**
957      * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
958      *
959      * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically
960      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
961      *
962      * @param context the context to use.
963      * @param entry the entry to bubble.
964      */
canLaunchInActivityView(Context context, NotificationEntry entry)965     static boolean canLaunchInActivityView(Context context, NotificationEntry entry) {
966         PendingIntent intent = entry.getBubbleMetadata() != null
967                 ? entry.getBubbleMetadata().getIntent()
968                 : null;
969         if (intent == null) {
970             Log.w(TAG, "Unable to create bubble -- no intent");
971             return false;
972         }
973         ActivityInfo info =
974                 intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
975         if (info == null) {
976             Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
977                     + intent);
978             return false;
979         }
980         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
981             Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
982                     + intent);
983             return false;
984         }
985         if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
986             Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always "
987                     + "for intent: " + intent);
988             return false;
989         }
990         if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) {
991             Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: "
992                     + intent);
993             return false;
994         }
995         return true;
996     }
997 
998     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
999     private class BubblesImeListener extends IPinnedStackListener.Stub {
1000 
1001         @Override
onListenerRegistered(IPinnedStackController controller)1002         public void onListenerRegistered(IPinnedStackController controller) throws RemoteException {
1003         }
1004 
1005         @Override
onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)1006         public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
1007                 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment,
1008                 int displayRotation) throws RemoteException {}
1009 
1010         @Override
onImeVisibilityChanged(boolean imeVisible, int imeHeight)1011         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
1012             if (mStackView != null && mStackView.getBubbleCount() > 0) {
1013                 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight));
1014             }
1015         }
1016 
1017         @Override
onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)1018         public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)
1019                 throws RemoteException {}
1020 
1021         @Override
onMinimizedStateChanged(boolean isMinimized)1022         public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {}
1023 
1024         @Override
onActionsChanged(ParceledListSlice actions)1025         public void onActionsChanged(ParceledListSlice actions) throws RemoteException {}
1026     }
1027 }
1028