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_BUBBLE;
20 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
21 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
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.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
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.NonNull;
46 import android.annotation.UserIdInt;
47 import android.app.ActivityManager.RunningTaskInfo;
48 import android.app.INotificationManager;
49 import android.app.Notification;
50 import android.app.NotificationChannel;
51 import android.app.NotificationManager;
52 import android.app.PendingIntent;
53 import android.content.Context;
54 import android.content.pm.ActivityInfo;
55 import android.content.pm.LauncherApps;
56 import android.content.pm.PackageManager;
57 import android.content.pm.ShortcutInfo;
58 import android.content.res.Configuration;
59 import android.graphics.PixelFormat;
60 import android.os.Binder;
61 import android.os.Handler;
62 import android.os.RemoteException;
63 import android.os.ServiceManager;
64 import android.os.UserHandle;
65 import android.service.notification.NotificationListenerService;
66 import android.service.notification.NotificationListenerService.RankingMap;
67 import android.service.notification.ZenModeConfig;
68 import android.util.ArraySet;
69 import android.util.Log;
70 import android.util.Pair;
71 import android.util.SparseSetArray;
72 import android.view.Display;
73 import android.view.View;
74 import android.view.ViewGroup;
75 import android.view.WindowManager;
76 
77 import androidx.annotation.IntDef;
78 import androidx.annotation.MainThread;
79 import androidx.annotation.Nullable;
80 
81 import com.android.internal.annotations.VisibleForTesting;
82 import com.android.internal.statusbar.IStatusBarService;
83 import com.android.internal.statusbar.NotificationVisibility;
84 import com.android.systemui.Dumpable;
85 import com.android.systemui.bubbles.dagger.BubbleModule;
86 import com.android.systemui.dump.DumpManager;
87 import com.android.systemui.model.SysUiState;
88 import com.android.systemui.plugins.statusbar.StatusBarStateController;
89 import com.android.systemui.shared.system.ActivityManagerWrapper;
90 import com.android.systemui.shared.system.PinnedStackListenerForwarder;
91 import com.android.systemui.shared.system.TaskStackChangeListener;
92 import com.android.systemui.shared.system.WindowManagerWrapper;
93 import com.android.systemui.statusbar.FeatureFlags;
94 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
95 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
96 import com.android.systemui.statusbar.ScrimView;
97 import com.android.systemui.statusbar.notification.NotificationChannelHelper;
98 import com.android.systemui.statusbar.notification.NotificationEntryListener;
99 import com.android.systemui.statusbar.notification.NotificationEntryManager;
100 import com.android.systemui.statusbar.notification.collection.NotifCollection;
101 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
102 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
103 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
104 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
105 import com.android.systemui.statusbar.phone.NotificationGroupManager;
106 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
107 import com.android.systemui.statusbar.phone.ScrimController;
108 import com.android.systemui.statusbar.phone.ShadeController;
109 import com.android.systemui.statusbar.phone.StatusBar;
110 import com.android.systemui.statusbar.policy.ConfigurationController;
111 import com.android.systemui.statusbar.policy.ZenModeController;
112 import com.android.systemui.util.FloatingContentCoordinator;
113 
114 import java.io.FileDescriptor;
115 import java.io.PrintWriter;
116 import java.lang.annotation.Retention;
117 import java.lang.annotation.Target;
118 import java.util.ArrayList;
119 import java.util.List;
120 import java.util.Objects;
121 
122 /**
123  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
124  * Bubbles can be expanded to show more content.
125  *
126  * The controller manages addition, removal, and visible state of bubbles on screen.
127  */
128 public class BubbleController implements ConfigurationController.ConfigurationListener, Dumpable {
129 
130     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
131 
132     @Retention(SOURCE)
133     @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
134             DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
135             DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT,
136             DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED})
137     @Target({FIELD, LOCAL_VARIABLE, PARAMETER})
138     @interface DismissReason {}
139 
140     static final int DISMISS_USER_GESTURE = 1;
141     static final int DISMISS_AGED = 2;
142     static final int DISMISS_TASK_FINISHED = 3;
143     static final int DISMISS_BLOCKED = 4;
144     static final int DISMISS_NOTIF_CANCEL = 5;
145     static final int DISMISS_ACCESSIBILITY_ACTION = 6;
146     static final int DISMISS_NO_LONGER_BUBBLE = 7;
147     static final int DISMISS_USER_CHANGED = 8;
148     static final int DISMISS_GROUP_CANCELLED = 9;
149     static final int DISMISS_INVALID_INTENT = 10;
150     static final int DISMISS_OVERFLOW_MAX_REACHED = 11;
151     static final int DISMISS_SHORTCUT_REMOVED = 12;
152     static final int DISMISS_PACKAGE_REMOVED = 13;
153 
154     private final Context mContext;
155     private final NotificationEntryManager mNotificationEntryManager;
156     private final NotifPipeline mNotifPipeline;
157     private final BubbleTaskStackListener mTaskStackListener;
158     private BubbleExpandListener mExpandListener;
159     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
160     private final NotificationGroupManager mNotificationGroupManager;
161     private final ShadeController mShadeController;
162     private final FloatingContentCoordinator mFloatingContentCoordinator;
163     private final BubbleDataRepository mDataRepository;
164     private BubbleLogger mLogger = new BubbleLoggerImpl();
165 
166     private BubbleData mBubbleData;
167     private ScrimView mBubbleScrim;
168     @Nullable private BubbleStackView mStackView;
169     private BubbleIconFactory mBubbleIconFactory;
170 
171     // Tracks the id of the current (foreground) user.
172     private int mCurrentUserId;
173     // Saves notification keys of active bubbles when users are switched.
174     private final SparseSetArray<String> mSavedBubbleKeysPerUser;
175 
176     // Used when ranking updates occur and we check if things should bubble / unbubble
177     private NotificationListenerService.Ranking mTmpRanking;
178 
179     // Bubbles get added to the status bar view
180     private final NotificationShadeWindowController mNotificationShadeWindowController;
181     private final ZenModeController mZenModeController;
182     private StatusBarStateListener mStatusBarStateListener;
183     private INotificationManager mINotificationManager;
184 
185     // Callback that updates BubbleOverflowActivity on data change.
186     @Nullable private Runnable mOverflowCallback = null;
187 
188     // Only load overflow data from disk once
189     private boolean mOverflowDataLoaded = false;
190 
191     /**
192      * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select
193      * this bubble and expand the stack.
194      */
195     @Nullable private NotificationEntry mNotifEntryToExpandOnShadeUnlock;
196 
197     private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
198     private IStatusBarService mBarService;
199     private WindowManager mWindowManager;
200     private SysUiState mSysUiState;
201 
202     // Used to post to main UI thread
203     private Handler mHandler = new Handler();
204 
205     /** LayoutParams used to add the BubbleStackView to the window manager. */
206     private WindowManager.LayoutParams mWmLayoutParams;
207     /** Whether or not the BubbleStackView has been added to the WindowManager. */
208     private boolean mAddedToWindowManager = false;
209 
210     /**
211      * Value from {@link NotificationShadeWindowController#getForceHasTopUi()} when we forced top UI
212      * due to expansion. We'll restore this value when the stack collapses.
213      */
214     private boolean mHadTopUi = false;
215 
216     // Listens to user switch so bubbles can be saved and restored.
217     private final NotificationLockscreenUserManager mNotifUserManager;
218 
219     /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
220     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
221 
222     /**
223      * Last known screen density, used to detect display size changes in {@link #onConfigChanged}.
224      */
225     private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
226 
227     /** Last known direction, used to detect layout direction changes @link #onConfigChanged}. */
228     private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
229 
230     private boolean mInflateSynchronously;
231 
232     // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline
233     private final List<NotifCallback> mCallbacks = new ArrayList<>();
234 
235     /**
236      * Whether the IME is visible, as reported by the BubbleStackView. If it is, we'll make the
237      * Bubbles window NOT_FOCUSABLE so that touches on the Bubbles UI doesn't steal focus from the
238      * ActivityView and hide the IME.
239      */
240     private boolean mImeVisible = false;
241 
242     /**
243      * Listener to find out about stack expansion / collapse events.
244      */
245     public interface BubbleExpandListener {
246         /**
247          * Called when the expansion state of the bubble stack changes.
248          *
249          * @param isExpanding whether it's expanding or collapsing
250          * @param key the notification key associated with bubble being expanded
251          */
onBubbleExpandChanged(boolean isExpanding, String key)252         void onBubbleExpandChanged(boolean isExpanding, String key);
253     }
254 
255     /**
256      * Listener to be notified when a bubbles' notification suppression state changes.
257      */
258     public interface NotificationSuppressionChangedListener {
259         /**
260          * Called when the notification suppression state of a bubble changes.
261          */
onBubbleNotificationSuppressionChange(Bubble bubble)262         void onBubbleNotificationSuppressionChange(Bubble bubble);
263     }
264 
265     /**
266      * Listener to be notified when a pending intent has been canceled for a bubble.
267      */
268     public interface PendingIntentCanceledListener {
269         /**
270          * Called when the pending intent for a bubble has been canceled.
271          */
onPendingIntentCanceled(Bubble bubble)272         void onPendingIntentCanceled(Bubble bubble);
273     }
274 
275     /**
276      * Callback for when the BubbleController wants to interact with the notification pipeline to:
277      * - Remove a previously bubbled notification
278      * - Update the notification shade since bubbled notification should/shouldn't be showing
279      */
280     public interface NotifCallback {
281         /**
282          * Called when a bubbled notification that was hidden from the shade is now being removed
283          * This can happen when an app cancels a bubbled notification or when the user dismisses a
284          * bubble.
285          */
removeNotification(@onNull NotificationEntry entry, int reason)286         void removeNotification(@NonNull NotificationEntry entry, int reason);
287 
288         /**
289          * Called when a bubbled notification has changed whether it should be
290          * filtered from the shade.
291          */
invalidateNotifications(@onNull String reason)292         void invalidateNotifications(@NonNull String reason);
293 
294         /**
295          * Called on a bubbled entry that has been removed when there are no longer
296          * bubbled entries in its group.
297          *
298          * Checks whether its group has any other (non-bubbled) children. If it doesn't,
299          * removes all remnants of the group's summary from the notification pipeline.
300          * TODO: (b/145659174) Only old pipeline needs this - delete post-migration.
301          */
maybeCancelSummary(@onNull NotificationEntry entry)302         void maybeCancelSummary(@NonNull NotificationEntry entry);
303     }
304 
305     /**
306      * Listens for the current state of the status bar and updates the visibility state
307      * of bubbles as needed.
308      */
309     private class StatusBarStateListener implements StatusBarStateController.StateListener {
310         private int mState;
311         /**
312          * Returns the current status bar state.
313          */
getCurrentState()314         public int getCurrentState() {
315             return mState;
316         }
317 
318         @Override
onStateChanged(int newState)319         public void onStateChanged(int newState) {
320             mState = newState;
321             boolean shouldCollapse = (mState != SHADE);
322             if (shouldCollapse) {
323                 collapseStack();
324             }
325 
326             if (mNotifEntryToExpandOnShadeUnlock != null) {
327                 expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
328                 mNotifEntryToExpandOnShadeUnlock = null;
329             }
330 
331             updateStack();
332         }
333     }
334 
335     /**
336      * Injected constructor. See {@link BubbleModule}.
337      */
BubbleController(Context context, NotificationShadeWindowController notificationShadeWindowController, StatusBarStateController statusBarStateController, ShadeController shadeController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, ConfigurationController configurationController, NotificationInterruptStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManager groupManager, NotificationEntryManager entryManager, NotifPipeline notifPipeline, FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, BubbleDataRepository dataRepository, SysUiState sysUiState, INotificationManager notificationManager, @Nullable IStatusBarService statusBarService, WindowManager windowManager, LauncherApps launcherApps)338     public BubbleController(Context context,
339             NotificationShadeWindowController notificationShadeWindowController,
340             StatusBarStateController statusBarStateController,
341             ShadeController shadeController,
342             BubbleData data,
343             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
344             ConfigurationController configurationController,
345             NotificationInterruptStateProvider interruptionStateProvider,
346             ZenModeController zenModeController,
347             NotificationLockscreenUserManager notifUserManager,
348             NotificationGroupManager groupManager,
349             NotificationEntryManager entryManager,
350             NotifPipeline notifPipeline,
351             FeatureFlags featureFlags,
352             DumpManager dumpManager,
353             FloatingContentCoordinator floatingContentCoordinator,
354             BubbleDataRepository dataRepository,
355             SysUiState sysUiState,
356             INotificationManager notificationManager,
357             @Nullable IStatusBarService statusBarService,
358             WindowManager windowManager,
359             LauncherApps launcherApps) {
360         dumpManager.registerDumpable(TAG, this);
361         mContext = context;
362         mShadeController = shadeController;
363         mNotificationInterruptStateProvider = interruptionStateProvider;
364         mNotifUserManager = notifUserManager;
365         mZenModeController = zenModeController;
366         mFloatingContentCoordinator = floatingContentCoordinator;
367         mDataRepository = dataRepository;
368         mINotificationManager = notificationManager;
369         mZenModeController.addCallback(new ZenModeController.Callback() {
370             @Override
371             public void onZenChanged(int zen) {
372                 for (Bubble b : mBubbleData.getBubbles()) {
373                     b.setShowDot(b.showInShade());
374                 }
375             }
376 
377             @Override
378             public void onConfigChanged(ZenModeConfig config) {
379                 for (Bubble b : mBubbleData.getBubbles()) {
380                     b.setShowDot(b.showInShade());
381                 }
382             }
383         });
384 
385         configurationController.addCallback(this /* configurationListener */);
386         mSysUiState = sysUiState;
387 
388         mBubbleData = data;
389         mBubbleData.setListener(mBubbleDataListener);
390         mBubbleData.setSuppressionChangedListener(new NotificationSuppressionChangedListener() {
391             @Override
392             public void onBubbleNotificationSuppressionChange(Bubble bubble) {
393                 // Make sure NoMan knows it's not showing in the shade anymore so anyone querying it
394                 // can tell.
395                 try {
396                     mBarService.onBubbleNotificationSuppressionChanged(bubble.getKey(),
397                             !bubble.showInShade());
398                 } catch (RemoteException e) {
399                     // Bad things have happened
400                 }
401             }
402         });
403         mBubbleData.setPendingIntentCancelledListener(bubble -> {
404             if (bubble.getBubbleIntent() == null) {
405                 return;
406             }
407             if (bubble.isIntentActive()) {
408                 bubble.setPendingIntentCanceled();
409                 return;
410             }
411             mHandler.post(
412                     () -> removeBubble(bubble.getKey(),
413                             BubbleController.DISMISS_INVALID_INTENT));
414         });
415 
416         mNotificationEntryManager = entryManager;
417         mNotificationGroupManager = groupManager;
418         mNotifPipeline = notifPipeline;
419 
420         if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
421             setupNEM();
422         } else {
423             setupNotifPipeline();
424         }
425 
426         mNotificationShadeWindowController = notificationShadeWindowController;
427         mStatusBarStateListener = new StatusBarStateListener();
428         statusBarStateController.addCallback(mStatusBarStateListener);
429 
430         mTaskStackListener = new BubbleTaskStackListener();
431         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
432 
433         try {
434             WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener());
435         } catch (RemoteException e) {
436             e.printStackTrace();
437         }
438         mSurfaceSynchronizer = synchronizer;
439 
440         mWindowManager = windowManager;
441         mBarService = statusBarService == null
442                 ? IStatusBarService.Stub.asInterface(
443                         ServiceManager.getService(Context.STATUS_BAR_SERVICE))
444                 : statusBarService;
445 
446         mBubbleScrim = new ScrimView(mContext);
447         mBubbleScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
448 
449         mSavedBubbleKeysPerUser = new SparseSetArray<>();
450         mCurrentUserId = mNotifUserManager.getCurrentUserId();
451         mNotifUserManager.addUserChangedListener(
452                 new NotificationLockscreenUserManager.UserChangedListener() {
453                     @Override
454                     public void onUserChanged(int newUserId) {
455                         BubbleController.this.saveBubbles(mCurrentUserId);
456                         mBubbleData.dismissAll(DISMISS_USER_CHANGED);
457                         BubbleController.this.restoreBubbles(newUserId);
458                         mCurrentUserId = newUserId;
459                     }
460                 });
461 
462         mBubbleIconFactory = new BubbleIconFactory(context);
463 
464         launcherApps.registerCallback(new LauncherApps.Callback() {
465             @Override
466             public void onPackageAdded(String s, UserHandle userHandle) {}
467 
468             @Override
469             public void onPackageChanged(String s, UserHandle userHandle) {}
470 
471             @Override
472             public void onPackageRemoved(String s, UserHandle userHandle) {
473                 // Remove bubbles with this package name, since it has been uninstalled and attempts
474                 // to open a bubble from an uninstalled app can cause issues.
475                 mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED);
476             }
477 
478             @Override
479             public void onPackagesAvailable(String[] strings, UserHandle userHandle,
480                     boolean b) {
481 
482             }
483 
484             @Override
485             public void onPackagesUnavailable(String[] packages, UserHandle userHandle,
486                     boolean b) {
487                 for (String packageName : packages) {
488                     // Remove bubbles from unavailable apps. This can occur when the app is on
489                     // external storage that has been removed.
490                     mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED);
491                 }
492             }
493 
494             @Override
495             public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts,
496                     UserHandle user) {
497                 super.onShortcutsChanged(packageName, validShortcuts, user);
498 
499                 // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts.
500                 mBubbleData.removeBubblesWithInvalidShortcuts(
501                         packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED);
502             }
503         });
504     }
505 
506     /**
507      * See {@link NotifCallback}.
508      */
addNotifCallback(NotifCallback callback)509     public void addNotifCallback(NotifCallback callback) {
510         mCallbacks.add(callback);
511     }
512 
513     /**
514      * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
515      */
hideCurrentInputMethod()516     public void hideCurrentInputMethod() {
517         try {
518             mBarService.hideCurrentInputMethodForBubbles();
519         } catch (RemoteException e) {
520             e.printStackTrace();
521         }
522     }
523 
setupNEM()524     private void setupNEM() {
525         mNotificationEntryManager.addNotificationEntryListener(
526                 new NotificationEntryListener() {
527                     @Override
528                     public void onPendingEntryAdded(NotificationEntry entry) {
529                         onEntryAdded(entry);
530                     }
531 
532                     @Override
533                     public void onPreEntryUpdated(NotificationEntry entry) {
534                         onEntryUpdated(entry);
535                     }
536 
537                     @Override
538                     public void onEntryRemoved(
539                             NotificationEntry entry,
540                             @android.annotation.Nullable NotificationVisibility visibility,
541                             boolean removedByUser,
542                             int reason) {
543                         BubbleController.this.onEntryRemoved(entry);
544                     }
545 
546                     @Override
547                     public void onNotificationRankingUpdated(RankingMap rankingMap) {
548                         onRankingUpdated(rankingMap);
549                     }
550                 });
551 
552         mNotificationEntryManager.addNotificationRemoveInterceptor(
553                 new NotificationRemoveInterceptor() {
554                     @Override
555                     public boolean onNotificationRemoveRequested(
556                             String key,
557                             NotificationEntry entry,
558                             int dismissReason) {
559                         final boolean isClearAll = dismissReason == REASON_CANCEL_ALL;
560                         final boolean isUserDimiss = dismissReason == REASON_CANCEL
561                                 || dismissReason == REASON_CLICK;
562                         final boolean isAppCancel = dismissReason == REASON_APP_CANCEL
563                                 || dismissReason == REASON_APP_CANCEL_ALL;
564                         final boolean isSummaryCancel =
565                                 dismissReason == REASON_GROUP_SUMMARY_CANCELED;
566 
567                         // Need to check for !appCancel here because the notification may have
568                         // previously been dismissed & entry.isRowDismissed would still be true
569                         boolean userRemovedNotif =
570                                 (entry != null && entry.isRowDismissed() && !isAppCancel)
571                                 || isClearAll || isUserDimiss || isSummaryCancel;
572 
573                         if (userRemovedNotif) {
574                             return handleDismissalInterception(entry);
575                         }
576                         return false;
577                     }
578                 });
579 
580         mNotificationGroupManager.addOnGroupChangeListener(
581                 new NotificationGroupManager.OnGroupChangeListener() {
582                     @Override
583                     public void onGroupSuppressionChanged(
584                             NotificationGroupManager.NotificationGroup group,
585                             boolean suppressed) {
586                         // More notifications could be added causing summary to no longer
587                         // be suppressed -- in this case need to remove the key.
588                         final String groupKey = group.summary != null
589                                 ? group.summary.getSbn().getGroupKey()
590                                 : null;
591                         if (!suppressed && groupKey != null
592                                 && mBubbleData.isSummarySuppressed(groupKey)) {
593                             mBubbleData.removeSuppressedSummary(groupKey);
594                         }
595                     }
596                 });
597 
598         addNotifCallback(new NotifCallback() {
599             @Override
600             public void removeNotification(NotificationEntry entry, int reason) {
601                 mNotificationEntryManager.performRemoveNotification(entry.getSbn(), reason);
602             }
603 
604             @Override
605             public void invalidateNotifications(String reason) {
606                 mNotificationEntryManager.updateNotifications(reason);
607             }
608 
609             @Override
610             public void maybeCancelSummary(NotificationEntry entry) {
611                 // Check if removed bubble has an associated suppressed group summary that needs
612                 // to be removed now.
613                 final String groupKey = entry.getSbn().getGroupKey();
614                 if (mBubbleData.isSummarySuppressed(groupKey)) {
615                     mBubbleData.removeSuppressedSummary(groupKey);
616 
617                     final NotificationEntry summary =
618                             mNotificationEntryManager.getActiveNotificationUnfiltered(
619                                     mBubbleData.getSummaryKey(groupKey));
620                     if (summary != null) {
621                         mNotificationEntryManager.performRemoveNotification(summary.getSbn(),
622                                 UNDEFINED_DISMISS_REASON);
623                     }
624                 }
625 
626                 // Check if we still need to remove the summary from NoManGroup because the summary
627                 // may not be in the mBubbleData.mSuppressedGroupKeys list and removed above.
628                 // For example:
629                 // 1. Bubbled notifications (group) is posted to shade and are visible bubbles
630                 // 2. User expands bubbles so now their respective notifications in the shade are
631                 // hidden, including the group summary
632                 // 3. User removes all bubbles
633                 // 4. We expect all the removed bubbles AND the summary (note: the summary was
634                 // never added to the suppressedSummary list in BubbleData, so we add this check)
635                 NotificationEntry summary =
636                         mNotificationGroupManager.getLogicalGroupSummary(entry.getSbn());
637                 if (summary != null) {
638                     ArrayList<NotificationEntry> summaryChildren =
639                             mNotificationGroupManager.getLogicalChildren(summary.getSbn());
640                     boolean isSummaryThisNotif = summary.getKey().equals(entry.getKey());
641                     if (!isSummaryThisNotif && (summaryChildren == null
642                             || summaryChildren.isEmpty())) {
643                         mNotificationEntryManager.performRemoveNotification(summary.getSbn(),
644                                 UNDEFINED_DISMISS_REASON);
645                     }
646                 }
647             }
648         });
649     }
650 
setupNotifPipeline()651     private void setupNotifPipeline() {
652         mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
653             @Override
654             public void onEntryAdded(NotificationEntry entry) {
655                 BubbleController.this.onEntryAdded(entry);
656             }
657 
658             @Override
659             public void onEntryUpdated(NotificationEntry entry) {
660                 BubbleController.this.onEntryUpdated(entry);
661             }
662 
663             @Override
664             public void onRankingUpdate(RankingMap rankingMap) {
665                 onRankingUpdated(rankingMap);
666             }
667 
668             @Override
669             public void onEntryRemoved(NotificationEntry entry,
670                     @NotifCollection.CancellationReason int reason) {
671                 BubbleController.this.onEntryRemoved(entry);
672             }
673         });
674     }
675 
676     /**
677      * Returns the scrim drawn behind the bubble stack. This is managed by {@link ScrimController}
678      * since we want the scrim's appearance and behavior to be identical to that of the notification
679      * shade scrim.
680      */
getScrimForBubble()681     public ScrimView getScrimForBubble() {
682         return mBubbleScrim;
683     }
684 
685     /**
686      * Called when the status bar has become visible or invisible (either permanently or
687      * temporarily).
688      */
onStatusBarVisibilityChanged(boolean visible)689     public void onStatusBarVisibilityChanged(boolean visible) {
690         if (mStackView != null) {
691             // Hide the stack temporarily if the status bar has been made invisible, and the stack
692             // is collapsed. An expanded stack should remain visible until collapsed.
693             mStackView.setTemporarilyInvisible(!visible && !isStackExpanded());
694         }
695     }
696 
697     /**
698      * Sets whether to perform inflation on the same thread as the caller. This method should only
699      * be used in tests, not in production.
700      */
701     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)702     void setInflateSynchronously(boolean inflateSynchronously) {
703         mInflateSynchronously = inflateSynchronously;
704     }
705 
setOverflowCallback(Runnable updateOverflow)706     void setOverflowCallback(Runnable updateOverflow) {
707         mOverflowCallback = updateOverflow;
708     }
709 
710     /**
711      * @return Bubbles for updating overflow.
712      */
getOverflowBubbles()713     List<Bubble> getOverflowBubbles() {
714         return mBubbleData.getOverflowBubbles();
715     }
716 
717     /**
718      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
719      * method initializes the stack view and adds it to the StatusBar just above the scrim.
720      */
ensureStackViewCreated()721     private void ensureStackViewCreated() {
722         if (mStackView == null) {
723             mStackView = new BubbleStackView(
724                     mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
725                     mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged,
726                     this::hideCurrentInputMethod);
727             mStackView.addView(mBubbleScrim);
728             if (mExpandListener != null) {
729                 mStackView.setExpandListener(mExpandListener);
730             }
731 
732             mStackView.setUnbubbleConversationCallback(key -> {
733                 final NotificationEntry entry =
734                         mNotificationEntryManager.getPendingOrActiveNotif(key);
735                 if (entry != null) {
736                     onUserChangedBubble(entry, false /* shouldBubble */);
737                 }
738             });
739         }
740 
741         addToWindowManagerMaybe();
742     }
743 
744     /** Adds the BubbleStackView to the WindowManager if it's not already there. */
addToWindowManagerMaybe()745     private void addToWindowManagerMaybe() {
746         // If the stack is null, or already added, don't add it.
747         if (mStackView == null || mAddedToWindowManager) {
748             return;
749         }
750 
751         mWmLayoutParams = new WindowManager.LayoutParams(
752                 // Fill the screen so we can use translation animations to position the bubble
753                 // stack. We'll use touchable regions to ignore touches that are not on the bubbles
754                 // themselves.
755                 ViewGroup.LayoutParams.MATCH_PARENT,
756                 ViewGroup.LayoutParams.MATCH_PARENT,
757                 WindowManager.LayoutParams.TYPE_TRUSTED_APPLICATION_OVERLAY,
758                 // Start not focusable - we'll become focusable when expanded so the ActivityView
759                 // can use the IME.
760                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
761                     | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
762                 PixelFormat.TRANSLUCENT);
763 
764         mWmLayoutParams.setFitInsetsTypes(0);
765         mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
766         mWmLayoutParams.token = new Binder();
767         mWmLayoutParams.setTitle("Bubbles!");
768         mWmLayoutParams.packageName = mContext.getPackageName();
769         mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
770 
771         try {
772             mAddedToWindowManager = true;
773             mWindowManager.addView(mStackView, mWmLayoutParams);
774         } catch (IllegalStateException e) {
775             // This means the stack has already been added. This shouldn't happen, since we keep
776             // track of that, but just in case, update the previously added view's layout params.
777             e.printStackTrace();
778             updateWmFlags();
779         }
780     }
781 
onImeVisibilityChanged(boolean imeVisible)782     private void onImeVisibilityChanged(boolean imeVisible) {
783         mImeVisible = imeVisible;
784         updateWmFlags();
785     }
786 
787     /** Removes the BubbleStackView from the WindowManager if it's there. */
removeFromWindowManagerMaybe()788     private void removeFromWindowManagerMaybe() {
789         if (!mAddedToWindowManager) {
790             return;
791         }
792 
793         try {
794             mAddedToWindowManager = false;
795             if (mStackView != null) {
796                 mWindowManager.removeView(mStackView);
797                 mStackView.removeView(mBubbleScrim);
798                 mStackView = null;
799             } else {
800                 Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
801             }
802         } catch (IllegalArgumentException e) {
803             // This means the stack has already been removed - it shouldn't happen, but ignore if it
804             // does, since we wanted it removed anyway.
805             e.printStackTrace();
806         }
807     }
808 
809     /**
810      * Updates the BubbleStackView's WindowManager.LayoutParams, and updates the WindowManager with
811      * the new params if the stack has been added.
812      */
updateWmFlags()813     private void updateWmFlags() {
814         if (mStackView == null) {
815             return;
816         }
817         if (isStackExpanded() && !mImeVisible) {
818             // If we're expanded, and the IME isn't visible, we want to be focusable. This ensures
819             // that any taps within Bubbles (including on the ActivityView) results in Bubbles
820             // receiving focus and clearing it from any other windows that might have it.
821             mWmLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
822         } else {
823             // If we're collapsed, we don't want to be focusable since tapping on the stack would
824             // steal focus from apps. We also don't want to be focusable if the IME is visible,
825             mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
826         }
827 
828         if (mAddedToWindowManager) {
829             try {
830                 mWindowManager.updateViewLayout(mStackView, mWmLayoutParams);
831             } catch (IllegalArgumentException e) {
832                 // If the stack is somehow not there, ignore the attempt to update it.
833                 e.printStackTrace();
834             }
835         }
836     }
837 
838     /**
839      * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
840      * added in the meantime.
841      */
onAllBubblesAnimatedOut()842     private void onAllBubblesAnimatedOut() {
843         if (mStackView != null) {
844             mStackView.setVisibility(INVISIBLE);
845             removeFromWindowManagerMaybe();
846         }
847     }
848 
849     /**
850      * Records the notification key for any active bubbles. These are used to restore active
851      * bubbles when the user returns to the foreground.
852      *
853      * @param userId the id of the user
854      */
saveBubbles(@serIdInt int userId)855     private void saveBubbles(@UserIdInt int userId) {
856         // First clear any existing keys that might be stored.
857         mSavedBubbleKeysPerUser.remove(userId);
858         // Add in all active bubbles for the current user.
859         for (Bubble bubble: mBubbleData.getBubbles()) {
860             mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
861         }
862     }
863 
864     /**
865      * Promotes existing notifications to Bubbles if they were previously bubbles.
866      *
867      * @param userId the id of the user
868      */
restoreBubbles(@serIdInt int userId)869     private void restoreBubbles(@UserIdInt int userId) {
870         ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId);
871         if (savedBubbleKeys == null) {
872             // There were no bubbles saved for this used.
873             return;
874         }
875         for (NotificationEntry e :
876                 mNotificationEntryManager.getActiveNotificationsForCurrentUser()) {
877             if (savedBubbleKeys.contains(e.getKey())
878                     && mNotificationInterruptStateProvider.shouldBubbleUp(e)
879                     && e.isBubble()
880                     && canLaunchInActivityView(mContext, e)) {
881                 updateBubble(e, true /* suppressFlyout */, false /* showInShade */);
882             }
883         }
884         // Finally, remove the entries for this user now that bubbles are restored.
885         mSavedBubbleKeysPerUser.remove(mCurrentUserId);
886     }
887 
888     @Override
onUiModeChanged()889     public void onUiModeChanged() {
890         updateForThemeChanges();
891     }
892 
893     @Override
onOverlayChanged()894     public void onOverlayChanged() {
895         updateForThemeChanges();
896     }
897 
updateForThemeChanges()898     private void updateForThemeChanges() {
899         if (mStackView != null) {
900             mStackView.onThemeChanged();
901         }
902         mBubbleIconFactory = new BubbleIconFactory(mContext);
903         // Reload each bubble
904         for (Bubble b: mBubbleData.getBubbles()) {
905             b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory,
906                     false /* skipInflation */);
907         }
908         for (Bubble b: mBubbleData.getOverflowBubbles()) {
909             b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory,
910                     false /* skipInflation */);
911         }
912     }
913 
914     @Override
onConfigChanged(Configuration newConfig)915     public void onConfigChanged(Configuration newConfig) {
916         if (mStackView != null && newConfig != null) {
917             if (newConfig.orientation != mOrientation) {
918                 mOrientation = newConfig.orientation;
919                 mStackView.onOrientationChanged(newConfig.orientation);
920             }
921             if (newConfig.densityDpi != mDensityDpi) {
922                 mDensityDpi = newConfig.densityDpi;
923                 mBubbleIconFactory = new BubbleIconFactory(mContext);
924                 mStackView.onDisplaySizeChanged();
925             }
926             if (newConfig.getLayoutDirection() != mLayoutDirection) {
927                 mLayoutDirection = newConfig.getLayoutDirection();
928                 mStackView.onLayoutDirectionChanged(mLayoutDirection);
929             }
930         }
931     }
932 
inLandscape()933     boolean inLandscape() {
934         return mOrientation == Configuration.ORIENTATION_LANDSCAPE;
935     }
936 
937     /**
938      * Set a listener to be notified of bubble expand events.
939      */
setExpandListener(BubbleExpandListener listener)940     public void setExpandListener(BubbleExpandListener listener) {
941         mExpandListener = ((isExpanding, key) -> {
942             if (listener != null) {
943                 listener.onBubbleExpandChanged(isExpanding, key);
944             }
945 
946             updateWmFlags();
947         });
948         if (mStackView != null) {
949             mStackView.setExpandListener(mExpandListener);
950         }
951     }
952 
953     /**
954      * Whether or not there are bubbles present, regardless of them being visible on the
955      * screen (e.g. if on AOD).
956      */
957     @VisibleForTesting
hasBubbles()958     boolean hasBubbles() {
959         if (mStackView == null) {
960             return false;
961         }
962         return mBubbleData.hasBubbles();
963     }
964 
965     /**
966      * Whether the stack of bubbles is expanded or not.
967      */
isStackExpanded()968     public boolean isStackExpanded() {
969         return mBubbleData.isExpanded();
970     }
971 
972     /**
973      * Tell the stack of bubbles to collapse.
974      */
collapseStack()975     public void collapseStack() {
976         mBubbleData.setExpanded(false /* expanded */);
977     }
978 
979     /**
980      * True if either:
981      * (1) There is a bubble associated with the provided key and if its notification is hidden
982      *     from the shade.
983      * (2) There is a group summary associated with the provided key that is hidden from the shade
984      *     because it has been dismissed but still has child bubbles active.
985      *
986      * False otherwise.
987      */
isBubbleNotificationSuppressedFromShade(NotificationEntry entry)988     public boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry) {
989         String key = entry.getKey();
990         boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
991                 && !mBubbleData.getAnyBubbleWithkey(key).showInShade());
992 
993         String groupKey = entry.getSbn().getGroupKey();
994         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
995         boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey));
996         return (isSummary && isSuppressedSummary) || isSuppressedBubble;
997     }
998 
999     /**
1000      * True if:
1001      * (1) The current notification entry same as selected bubble notification entry and the
1002      * stack is currently expanded.
1003      *
1004      * False otherwise.
1005      */
isBubbleExpanded(NotificationEntry entry)1006     public boolean isBubbleExpanded(NotificationEntry entry) {
1007         return isStackExpanded() && mBubbleData != null && mBubbleData.getSelectedBubble() != null
1008                 && mBubbleData.getSelectedBubble().getKey().equals(entry.getKey()) ? true : false;
1009     }
1010 
promoteBubbleFromOverflow(Bubble bubble)1011     void promoteBubbleFromOverflow(Bubble bubble) {
1012         mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
1013         bubble.setInflateSynchronously(mInflateSynchronously);
1014         bubble.setShouldAutoExpand(true);
1015         bubble.markAsAccessedAt(System.currentTimeMillis());
1016         setIsBubble(bubble, true /* isBubble */);
1017     }
1018 
1019     /**
1020      * Request the stack expand if needed, then select the specified Bubble as current.
1021      * If no bubble exists for this entry, one is created.
1022      *
1023      * @param entry the notification for the bubble to be selected
1024      */
expandStackAndSelectBubble(NotificationEntry entry)1025     public void expandStackAndSelectBubble(NotificationEntry entry) {
1026         if (mStatusBarStateListener.getCurrentState() == SHADE) {
1027             mNotifEntryToExpandOnShadeUnlock = null;
1028 
1029             String key = entry.getKey();
1030             Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
1031             if (bubble != null) {
1032                 mBubbleData.setSelectedBubble(bubble);
1033                 mBubbleData.setExpanded(true);
1034             } else {
1035                 bubble = mBubbleData.getOverflowBubbleWithKey(key);
1036                 if (bubble != null) {
1037                     promoteBubbleFromOverflow(bubble);
1038                 } else if (entry.canBubble()) {
1039                     // It can bubble but it's not -- it got aged out of the overflow before it
1040                     // was dismissed or opened, make it a bubble again.
1041                     setIsBubble(entry, true /* isBubble */, true /* autoExpand */);
1042                 }
1043             }
1044         } else {
1045             // Wait until we're unlocked to expand, so that the user can see the expand animation
1046             // and also to work around bugs with expansion animation + shade unlock happening at the
1047             // same time.
1048             mNotifEntryToExpandOnShadeUnlock = entry;
1049         }
1050     }
1051 
1052     /**
1053      * When a notification is marked Priority, expand the stack if needed,
1054      * then (maybe create and) select the given bubble.
1055      *
1056      * @param entry the notification for the bubble to show
1057      */
onUserChangedImportance(NotificationEntry entry)1058     public void onUserChangedImportance(NotificationEntry entry) {
1059         try {
1060             int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1061             flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
1062             mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags);
1063         } catch (RemoteException e) {
1064             Log.e(TAG, e.getMessage());
1065         }
1066         mShadeController.collapsePanel(true);
1067         if (entry.getRow() != null) {
1068             entry.getRow().updateBubbleButton();
1069         }
1070     }
1071 
1072     /**
1073      * Directs a back gesture at the bubble stack. When opened, the current expanded bubble
1074      * is forwarded a back key down/up pair.
1075      */
performBackPressIfNeeded()1076     public void performBackPressIfNeeded() {
1077         if (mStackView != null) {
1078             mStackView.performBackPressIfNeeded();
1079         }
1080     }
1081 
1082     /**
1083      * Adds or updates a bubble associated with the provided notification entry.
1084      *
1085      * @param notif the notification associated with this bubble.
1086      */
updateBubble(NotificationEntry notif)1087     void updateBubble(NotificationEntry notif) {
1088         updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
1089     }
1090 
1091     /**
1092      * Fills the overflow bubbles by loading them from disk.
1093      */
loadOverflowBubblesFromDisk()1094     void loadOverflowBubblesFromDisk() {
1095         if (!mBubbleData.getOverflowBubbles().isEmpty() || mOverflowDataLoaded) {
1096             // we don't need to load overflow bubbles from disk if it is already in memory
1097             return;
1098         }
1099         mOverflowDataLoaded = true;
1100         mDataRepository.loadBubbles((bubbles) -> {
1101             bubbles.forEach(bubble -> {
1102                 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
1103                     // if the bubble is already active, there's no need to push it to overflow
1104                     return;
1105                 }
1106                 bubble.inflate((b) -> mBubbleData.overflowBubble(DISMISS_AGED, bubble),
1107                         mContext, mStackView, mBubbleIconFactory, true /* skipInflation */);
1108             });
1109             return null;
1110         });
1111     }
1112 
updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade)1113     void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
1114         // If this is an interruptive notif, mark that it's interrupted
1115         if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
1116             notif.setInterruption();
1117         }
1118         Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
1119         inflateAndAdd(bubble, suppressFlyout, showInShade);
1120     }
1121 
inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade)1122     void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
1123         // Lazy init stack view when a bubble is created
1124         ensureStackViewCreated();
1125         bubble.setInflateSynchronously(mInflateSynchronously);
1126         bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
1127                 mContext, mStackView, mBubbleIconFactory, false /* skipInflation */);
1128     }
1129 
1130     /**
1131      * Called when a user has indicated that an active notification should be shown as a bubble.
1132      * <p>
1133      * This method will collapse the shade, create the bubble without a flyout or dot, and suppress
1134      * the notification from appearing in the shade.
1135      *
1136      * @param entry the notification to change bubble state for.
1137      * @param shouldBubble whether the notification should show as a bubble or not.
1138      */
onUserChangedBubble(@onNull final NotificationEntry entry, boolean shouldBubble)1139     public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) {
1140         NotificationChannel channel = entry.getChannel();
1141         final String appPkg = entry.getSbn().getPackageName();
1142         final int appUid = entry.getSbn().getUid();
1143         if (channel == null || appPkg == null) {
1144             return;
1145         }
1146 
1147         // Update the state in NotificationManagerService
1148         try {
1149             int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1150             flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
1151             mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags);
1152         } catch (RemoteException e) {
1153         }
1154 
1155         // Change the settings
1156         channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext,
1157                 mINotificationManager, entry, channel);
1158         channel.setAllowBubbles(shouldBubble);
1159         try {
1160             int currentPref = mINotificationManager.getBubblePreferenceForPackage(appPkg, appUid);
1161             if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) {
1162                 mINotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED);
1163             }
1164             mINotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel);
1165         } catch (RemoteException e) {
1166             Log.e(TAG, e.getMessage());
1167         }
1168 
1169         if (shouldBubble) {
1170             mShadeController.collapsePanel(true);
1171             if (entry.getRow() != null) {
1172                 entry.getRow().updateBubbleButton();
1173             }
1174         }
1175     }
1176 
1177     /**
1178      * Removes the bubble with the given key.
1179      * <p>
1180      * Must be called from the main thread.
1181      */
1182     @MainThread
removeBubble(String key, int reason)1183     void removeBubble(String key, int reason) {
1184         if (mBubbleData.hasAnyBubbleWithKey(key)) {
1185             mBubbleData.dismissBubbleWithKey(key, reason);
1186         }
1187     }
1188 
onEntryAdded(NotificationEntry entry)1189     private void onEntryAdded(NotificationEntry entry) {
1190         if (mNotificationInterruptStateProvider.shouldBubbleUp(entry)
1191                 && entry.isBubble()
1192                 && canLaunchInActivityView(mContext, entry)) {
1193             updateBubble(entry);
1194         }
1195     }
1196 
onEntryUpdated(NotificationEntry entry)1197     private void onEntryUpdated(NotificationEntry entry) {
1198         // shouldBubbleUp checks canBubble & for bubble metadata
1199         boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry)
1200                 && canLaunchInActivityView(mContext, entry);
1201         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
1202             // It was previously a bubble but no longer a bubble -- lets remove it
1203             removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
1204         } else if (shouldBubble && entry.isBubble()) {
1205             updateBubble(entry);
1206         }
1207     }
1208 
onEntryRemoved(NotificationEntry entry)1209     private void onEntryRemoved(NotificationEntry entry) {
1210         if (isSummaryOfBubbles(entry)) {
1211             final String groupKey = entry.getSbn().getGroupKey();
1212             mBubbleData.removeSuppressedSummary(groupKey);
1213 
1214             // Remove any associated bubble children with the summary
1215             final List<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(
1216                     groupKey, mNotificationEntryManager);
1217             for (int i = 0; i < bubbleChildren.size(); i++) {
1218                 removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED);
1219             }
1220         } else {
1221             removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL);
1222         }
1223     }
1224 
1225     /**
1226      * Called when NotificationListener has received adjusted notification rank and reapplied
1227      * filtering and sorting. This is used to dismiss or create bubbles based on changes in
1228      * permissions on the notification channel or the global setting.
1229      *
1230      * @param rankingMap the updated ranking map from NotificationListenerService
1231      */
onRankingUpdated(RankingMap rankingMap)1232     private void onRankingUpdated(RankingMap rankingMap) {
1233         if (mTmpRanking == null) {
1234             mTmpRanking = new NotificationListenerService.Ranking();
1235         }
1236         String[] orderedKeys = rankingMap.getOrderedKeys();
1237         for (int i = 0; i < orderedKeys.length; i++) {
1238             String key = orderedKeys[i];
1239             NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key);
1240             rankingMap.getRanking(key, mTmpRanking);
1241             boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key);
1242             if (isActiveBubble && !mTmpRanking.canBubble()) {
1243                 mBubbleData.dismissBubbleWithKey(entry.getKey(),
1244                         BubbleController.DISMISS_BLOCKED);
1245             } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) {
1246                 entry.setFlagBubble(true);
1247                 onEntryUpdated(entry);
1248             }
1249         }
1250     }
1251 
setIsBubble(@onNull final NotificationEntry entry, final boolean isBubble, final boolean autoExpand)1252     private void setIsBubble(@NonNull final NotificationEntry entry, final boolean isBubble,
1253             final boolean autoExpand) {
1254         Objects.requireNonNull(entry);
1255         if (isBubble) {
1256             entry.getSbn().getNotification().flags |= FLAG_BUBBLE;
1257         } else {
1258             entry.getSbn().getNotification().flags &= ~FLAG_BUBBLE;
1259         }
1260         try {
1261             int flags = 0;
1262             if (autoExpand) {
1263                 flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1264                 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
1265             }
1266             mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags);
1267         } catch (RemoteException e) {
1268             // Bad things have happened
1269         }
1270     }
1271 
setIsBubble(@onNull final Bubble b, final boolean isBubble)1272     private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) {
1273         Objects.requireNonNull(b);
1274         b.setIsBubble(isBubble);
1275         final NotificationEntry entry = mNotificationEntryManager
1276                 .getPendingOrActiveNotif(b.getKey());
1277         if (entry != null) {
1278             // Updating the entry to be a bubble will trigger our normal update flow
1279             setIsBubble(entry, isBubble, b.shouldAutoExpand());
1280         } else if (isBubble) {
1281             // If bubble doesn't exist, it's a persisted bubble so we need to add it to the
1282             // stack ourselves
1283             Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */);
1284             inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */,
1285                     !bubble.shouldAutoExpand() /* showInShade */);
1286         }
1287     }
1288 
1289     @SuppressWarnings("FieldCanBeLocal")
1290     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
1291 
1292         @Override
1293         public void applyUpdate(BubbleData.Update update) {
1294             ensureStackViewCreated();
1295 
1296             // Lazy load overflow bubbles from disk
1297             loadOverflowBubblesFromDisk();
1298             // Update bubbles in overflow.
1299             if (mOverflowCallback != null) {
1300                 mOverflowCallback.run();
1301             }
1302 
1303             // Collapsing? Do this first before remaining steps.
1304             if (update.expandedChanged && !update.expanded) {
1305                 mStackView.setExpanded(false);
1306                 mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi);
1307             }
1308 
1309             // Do removals, if any.
1310             ArrayList<Pair<Bubble, Integer>> removedBubbles =
1311                     new ArrayList<>(update.removedBubbles);
1312             ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>();
1313             for (Pair<Bubble, Integer> removed : removedBubbles) {
1314                 final Bubble bubble = removed.first;
1315                 @DismissReason final int reason = removed.second;
1316 
1317                 if (mStackView != null) {
1318                     mStackView.removeBubble(bubble);
1319                 }
1320 
1321                 // If the bubble is removed for user switching, leave the notification in place.
1322                 if (reason == DISMISS_USER_CHANGED) {
1323                     continue;
1324                 }
1325                 if (reason == DISMISS_NOTIF_CANCEL) {
1326                     bubblesToBeRemovedFromRepository.add(bubble);
1327                 }
1328                 final NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(
1329                         bubble.getKey());
1330                 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
1331                     if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())
1332                         && (!bubble.showInShade()
1333                             || reason == DISMISS_NOTIF_CANCEL
1334                             || reason == DISMISS_GROUP_CANCELLED)) {
1335                         // The bubble is now gone & the notification is hidden from the shade, so
1336                         // time to actually remove it
1337                         for (NotifCallback cb : mCallbacks) {
1338                             if (entry != null) {
1339                                 cb.removeNotification(entry, REASON_CANCEL);
1340                             }
1341                         }
1342                     } else {
1343                         if (bubble.isBubble()) {
1344                             setIsBubble(bubble, false /* isBubble */);
1345                         }
1346                         if (entry != null && entry.getRow() != null) {
1347                             entry.getRow().updateBubbleButton();
1348                         }
1349                     }
1350 
1351                 }
1352                 if (entry != null) {
1353                     final String groupKey = entry.getSbn().getGroupKey();
1354                     if (mBubbleData.getBubblesInGroup(
1355                             groupKey, mNotificationEntryManager).isEmpty()) {
1356                         // Time to potentially remove the summary
1357                         for (NotifCallback cb : mCallbacks) {
1358                             cb.maybeCancelSummary(entry);
1359                         }
1360                     }
1361                 }
1362             }
1363             mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository);
1364 
1365             if (update.addedBubble != null && mStackView != null) {
1366                 mDataRepository.addBubble(mCurrentUserId, update.addedBubble);
1367                 mStackView.addBubble(update.addedBubble);
1368             }
1369 
1370             if (update.updatedBubble != null && mStackView != null) {
1371                 mStackView.updateBubble(update.updatedBubble);
1372             }
1373 
1374             // At this point, the correct bubbles are inflated in the stack.
1375             // Make sure the order in bubble data is reflected in bubble row.
1376             if (update.orderChanged && mStackView != null) {
1377                 mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
1378                 mStackView.updateBubbleOrder(update.bubbles);
1379             }
1380 
1381             if (update.selectionChanged && mStackView != null) {
1382                 mStackView.setSelectedBubble(update.selectedBubble);
1383                 if (update.selectedBubble != null) {
1384                     final NotificationEntry entry = mNotificationEntryManager
1385                             .getPendingOrActiveNotif(update.selectedBubble.getKey());
1386                     if (entry != null) {
1387                         mNotificationGroupManager.updateSuppression(entry);
1388                     }
1389                 }
1390             }
1391 
1392             // Expanding? Apply this last.
1393             if (update.expandedChanged && update.expanded) {
1394                 if (mStackView != null) {
1395                     mStackView.setExpanded(true);
1396                     mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi();
1397                     mNotificationShadeWindowController.setForceHasTopUi(true);
1398                 }
1399             }
1400 
1401             for (NotifCallback cb : mCallbacks) {
1402                 cb.invalidateNotifications("BubbleData.Listener.applyUpdate");
1403             }
1404             updateStack();
1405 
1406             if (DEBUG_BUBBLE_CONTROLLER) {
1407                 Log.d(TAG, "\n[BubbleData] bubbles:");
1408                 Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getBubbles(),
1409                         mBubbleData.getSelectedBubble()));
1410 
1411                 if (mStackView != null) {
1412                     Log.d(TAG, "\n[BubbleStackView]");
1413                     Log.d(TAG, BubbleDebugConfig.formatBubblesString(mStackView.getBubblesOnScreen(),
1414                             mStackView.getExpandedBubble()));
1415                 }
1416                 Log.d(TAG, "\n[BubbleData] overflow:");
1417                 Log.d(TAG, BubbleDebugConfig.formatBubblesString(mBubbleData.getOverflowBubbles(),
1418                         null) + "\n");
1419             }
1420         }
1421     };
1422 
1423     /**
1424      * We intercept notification entries (including group summaries) dismissed by the user when
1425      * there is an active bubble associated with it. We do this so that developers can still
1426      * cancel it (and hence the bubbles associated with it). However, these intercepted
1427      * notifications should then be hidden from the shade since the user has cancelled them, so we
1428      *  {@link Bubble#setSuppressNotification}.  For the case of suppressed summaries, we also add
1429      *  {@link BubbleData#addSummaryToSuppress}.
1430      *
1431      * @return true if we want to intercept the dismissal of the entry, else false.
1432      */
handleDismissalInterception(NotificationEntry entry)1433     public boolean handleDismissalInterception(NotificationEntry entry) {
1434         if (entry == null) {
1435             return false;
1436         }
1437         if (isSummaryOfBubbles(entry)) {
1438             handleSummaryDismissalInterception(entry);
1439         } else {
1440             Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey());
1441             if (bubble == null || !entry.isBubble()) {
1442                 bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey());
1443             }
1444             if (bubble == null) {
1445                 return false;
1446             }
1447             bubble.setSuppressNotification(true);
1448             bubble.setShowDot(false /* show */);
1449         }
1450         // Update the shade
1451         for (NotifCallback cb : mCallbacks) {
1452             cb.invalidateNotifications("BubbleController.handleDismissalInterception");
1453         }
1454         return true;
1455     }
1456 
isSummaryOfBubbles(NotificationEntry entry)1457     private boolean isSummaryOfBubbles(NotificationEntry entry) {
1458         if (entry == null) {
1459             return false;
1460         }
1461 
1462         String groupKey = entry.getSbn().getGroupKey();
1463         ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup(
1464                 groupKey, mNotificationEntryManager);
1465         boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey)
1466                 && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey()));
1467         boolean isSummary = entry.getSbn().getNotification().isGroupSummary();
1468         return (isSuppressedSummary || isSummary)
1469                 && bubbleChildren != null
1470                 && !bubbleChildren.isEmpty();
1471     }
1472 
handleSummaryDismissalInterception(NotificationEntry summary)1473     private void handleSummaryDismissalInterception(NotificationEntry summary) {
1474         // current children in the row:
1475         final List<NotificationEntry> children = summary.getAttachedNotifChildren();
1476         if (children != null) {
1477             for (int i = 0; i < children.size(); i++) {
1478                 NotificationEntry child = children.get(i);
1479                 if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) {
1480                     // Suppress the bubbled child
1481                     // As far as group manager is concerned, once a child is no longer shown
1482                     // in the shade, it is essentially removed.
1483                     Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey());
1484                     if (bubbleChild != null) {
1485                         final NotificationEntry entry = mNotificationEntryManager
1486                                 .getPendingOrActiveNotif(bubbleChild.getKey());
1487                         if (entry != null) {
1488                             mNotificationGroupManager.onEntryRemoved(entry);
1489                         }
1490                         bubbleChild.setSuppressNotification(true);
1491                         bubbleChild.setShowDot(false /* show */);
1492                     }
1493                 } else {
1494                     // non-bubbled children can be removed
1495                     for (NotifCallback cb : mCallbacks) {
1496                         cb.removeNotification(child, REASON_GROUP_SUMMARY_CANCELED);
1497                     }
1498                 }
1499             }
1500         }
1501 
1502         // And since all children are removed, remove the summary.
1503         mNotificationGroupManager.onEntryRemoved(summary);
1504 
1505         // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated
1506         mBubbleData.addSummaryToSuppress(summary.getSbn().getGroupKey(),
1507                 summary.getKey());
1508     }
1509 
1510     /**
1511      * Updates the visibility of the bubbles based on current state.
1512      * Does not un-bubble, just hides or un-hides.
1513      * Updates stack description for TalkBack focus.
1514      */
updateStack()1515     public void updateStack() {
1516         if (mStackView == null) {
1517             return;
1518         }
1519 
1520         if (mStatusBarStateListener.getCurrentState() != SHADE) {
1521             // Bubbles don't appear over the locked shade.
1522             mStackView.setVisibility(INVISIBLE);
1523         } else if (hasBubbles()) {
1524             // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
1525             // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
1526             // out.
1527             mStackView.setVisibility(VISIBLE);
1528         }
1529 
1530         mStackView.updateContentDescription();
1531     }
1532 
1533     /**
1534      * The display id of the expanded view, if the stack is expanded and not occluded by the
1535      * status bar, otherwise returns {@link Display#INVALID_DISPLAY}.
1536      */
getExpandedDisplayId(Context context)1537     public int getExpandedDisplayId(Context context) {
1538         if (mStackView == null) {
1539             return INVALID_DISPLAY;
1540         }
1541         final boolean defaultDisplay = context.getDisplay() != null
1542                 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY;
1543         final BubbleViewProvider expandedViewProvider = mStackView.getExpandedBubble();
1544         if (defaultDisplay && expandedViewProvider != null && isStackExpanded()
1545                 && !mNotificationShadeWindowController.getPanelExpanded()) {
1546             return expandedViewProvider.getDisplayId();
1547         }
1548         return INVALID_DISPLAY;
1549     }
1550 
1551     @VisibleForTesting
getStackView()1552     BubbleStackView getStackView() {
1553         return mStackView;
1554     }
1555 
1556     /**
1557      * Description of current bubble state.
1558      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)1559     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1560         pw.println("BubbleController state:");
1561         mBubbleData.dump(fd, pw, args);
1562         pw.println();
1563         if (mStackView != null) {
1564             mStackView.dump(fd, pw, args);
1565         }
1566         pw.println();
1567     }
1568 
1569     /**
1570      * This task stack listener is responsible for responding to tasks moved to the front
1571      * which are on the default (main) display. When this happens, expanded bubbles must be
1572      * collapsed so the user may interact with the app which was just moved to the front.
1573      * <p>
1574      * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches
1575      * these calls via a main thread Handler.
1576      */
1577     @MainThread
1578     private class BubbleTaskStackListener extends TaskStackChangeListener {
1579 
1580         @Override
onTaskMovedToFront(RunningTaskInfo taskInfo)1581         public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
1582             if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) {
1583                 if (!mStackView.isExpansionAnimating()) {
1584                     mBubbleData.setExpanded(false);
1585                 }
1586             }
1587         }
1588 
1589         @Override
onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible)1590         public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible,
1591                 boolean clearedTask, boolean wasVisible) {
1592             for (Bubble b : mBubbleData.getBubbles()) {
1593                 if (b.getDisplayId() == task.displayId) {
1594                     mBubbleData.setSelectedBubble(b);
1595                     mBubbleData.setExpanded(true);
1596                     return;
1597                 }
1598             }
1599         }
1600 
1601         @Override
onActivityLaunchOnSecondaryDisplayRerouted()1602         public void onActivityLaunchOnSecondaryDisplayRerouted() {
1603             if (mStackView != null) {
1604                 mBubbleData.setExpanded(false);
1605             }
1606         }
1607 
1608         @Override
onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)1609         public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {
1610             if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) {
1611                 if (mImeVisible) {
1612                     hideCurrentInputMethod();
1613                 } else {
1614                     mBubbleData.setExpanded(false);
1615                 }
1616             }
1617         }
1618 
1619         @Override
onSingleTaskDisplayDrawn(int displayId)1620         public void onSingleTaskDisplayDrawn(int displayId) {
1621             if (mStackView == null) {
1622                 return;
1623             }
1624             mStackView.showExpandedViewContents(displayId);
1625         }
1626 
1627         @Override
onSingleTaskDisplayEmpty(int displayId)1628         public void onSingleTaskDisplayEmpty(int displayId) {
1629             final BubbleViewProvider expandedBubble = mStackView != null
1630                     ? mStackView.getExpandedBubble()
1631                     : null;
1632             int expandedId = expandedBubble != null ? expandedBubble.getDisplayId() : -1;
1633             if (mStackView != null && mStackView.isExpanded() && expandedId == displayId) {
1634                 mBubbleData.setExpanded(false);
1635             }
1636             mBubbleData.notifyDisplayEmpty(displayId);
1637         }
1638     }
1639 
1640     /**
1641      * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
1642      *
1643      * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically
1644      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
1645      *
1646      * @param context the context to use.
1647      * @param entry the entry to bubble.
1648      */
canLaunchInActivityView(Context context, NotificationEntry entry)1649     static boolean canLaunchInActivityView(Context context, NotificationEntry entry) {
1650         PendingIntent intent = entry.getBubbleMetadata() != null
1651                 ? entry.getBubbleMetadata().getIntent()
1652                 : null;
1653         if (entry.getBubbleMetadata() != null
1654                 && entry.getBubbleMetadata().getShortcutId() != null) {
1655             return true;
1656         }
1657         if (intent == null) {
1658             Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
1659             return false;
1660         }
1661         PackageManager packageManager = StatusBar.getPackageManagerForUser(
1662                 context, entry.getSbn().getUser().getIdentifier());
1663         ActivityInfo info =
1664                 intent.getIntent().resolveActivityInfo(packageManager, 0);
1665         if (info == null) {
1666             Log.w(TAG, "Unable to send as bubble, "
1667                     + entry.getKey() + " couldn't find activity info for intent: "
1668                     + intent);
1669             return false;
1670         }
1671         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
1672             Log.w(TAG, "Unable to send as bubble, "
1673                     + entry.getKey() + " activity is not resizable for intent: "
1674                     + intent);
1675             return false;
1676         }
1677         return true;
1678     }
1679 
1680     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
1681     private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedStackListener {
1682         @Override
onImeVisibilityChanged(boolean imeVisible, int imeHeight)1683         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
1684             if (mStackView != null) {
1685                 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight));
1686             }
1687         }
1688     }
1689 }
1690