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