• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles;
18 
19 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED;
21 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
22 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
23 import static android.view.View.INVISIBLE;
24 import static android.view.View.VISIBLE;
25 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
26 
27 import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE;
28 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER;
29 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_GESTURE;
30 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
31 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
32 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED;
33 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED;
34 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT;
35 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL;
36 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_BUBBLE_UP;
37 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE;
38 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED;
39 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED;
40 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED;
41 
42 import android.annotation.NonNull;
43 import android.annotation.UserIdInt;
44 import android.app.ActivityManager;
45 import android.app.Notification;
46 import android.app.NotificationChannel;
47 import android.app.PendingIntent;
48 import android.content.BroadcastReceiver;
49 import android.content.Context;
50 import android.content.Intent;
51 import android.content.IntentFilter;
52 import android.content.pm.ActivityInfo;
53 import android.content.pm.LauncherApps;
54 import android.content.pm.PackageManager;
55 import android.content.pm.ShortcutInfo;
56 import android.content.pm.UserInfo;
57 import android.content.res.Configuration;
58 import android.graphics.PixelFormat;
59 import android.graphics.Rect;
60 import android.os.Binder;
61 import android.os.Handler;
62 import android.os.RemoteException;
63 import android.os.ServiceManager;
64 import android.os.SystemProperties;
65 import android.os.UserHandle;
66 import android.os.UserManager;
67 import android.service.notification.NotificationListenerService;
68 import android.service.notification.NotificationListenerService.RankingMap;
69 import android.util.Log;
70 import android.util.Pair;
71 import android.util.SparseArray;
72 import android.view.View;
73 import android.view.ViewGroup;
74 import android.view.WindowInsets;
75 import android.view.WindowManager;
76 
77 import androidx.annotation.MainThread;
78 import androidx.annotation.Nullable;
79 
80 import com.android.internal.annotations.VisibleForTesting;
81 import com.android.internal.statusbar.IStatusBarService;
82 import com.android.wm.shell.ShellTaskOrganizer;
83 import com.android.wm.shell.TaskViewTransitions;
84 import com.android.wm.shell.WindowManagerShellWrapper;
85 import com.android.wm.shell.common.DisplayController;
86 import com.android.wm.shell.common.FloatingContentCoordinator;
87 import com.android.wm.shell.common.ShellExecutor;
88 import com.android.wm.shell.common.SyncTransactionQueue;
89 import com.android.wm.shell.common.TaskStackListenerCallback;
90 import com.android.wm.shell.common.TaskStackListenerImpl;
91 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
92 import com.android.wm.shell.common.annotations.ShellMainThread;
93 import com.android.wm.shell.draganddrop.DragAndDropController;
94 import com.android.wm.shell.onehanded.OneHandedController;
95 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
96 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
97 import com.android.wm.shell.sysui.ConfigurationChangeListener;
98 import com.android.wm.shell.sysui.ShellCommandHandler;
99 import com.android.wm.shell.sysui.ShellController;
100 import com.android.wm.shell.sysui.ShellInit;
101 
102 import java.io.PrintWriter;
103 import java.util.ArrayList;
104 import java.util.HashMap;
105 import java.util.HashSet;
106 import java.util.List;
107 import java.util.Map;
108 import java.util.Objects;
109 import java.util.Optional;
110 import java.util.Set;
111 import java.util.concurrent.Executor;
112 import java.util.function.Consumer;
113 import java.util.function.IntConsumer;
114 
115 /**
116  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
117  * Bubbles can be expanded to show more content.
118  *
119  * The controller manages addition, removal, and visible state of bubbles on screen.
120  */
121 public class BubbleController implements ConfigurationChangeListener {
122 
123     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
124 
125     // Should match with PhoneWindowManager
126     private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
127     private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav";
128 
129     // TODO(b/256873975) Should use proper flag when available to shell/launcher
130     /**
131      * Whether bubbles are showing in the bubble bar from launcher. This is only available
132      * on large screens and {@link BubbleController#isShowingAsBubbleBar()} should be used
133      * to check all conditions that indicate if the bubble bar is in use.
134      */
135     private static final boolean BUBBLE_BAR_ENABLED =
136             SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
137 
138 
139     /**
140      * Common interface to send updates to bubble views.
141      */
142     public interface BubbleViewCallback {
143         /** Called when the provided bubble should be removed. */
removeBubble(Bubble removedBubble)144         void removeBubble(Bubble removedBubble);
145         /** Called when the provided bubble should be added. */
addBubble(Bubble addedBubble)146         void addBubble(Bubble addedBubble);
147         /** Called when the provided bubble should be updated. */
updateBubble(Bubble updatedBubble)148         void updateBubble(Bubble updatedBubble);
149         /** Called when the provided bubble should be selected. */
selectionChanged(BubbleViewProvider selectedBubble)150         void selectionChanged(BubbleViewProvider selectedBubble);
151         /** Called when the provided bubble's suppression state has changed. */
suppressionChanged(Bubble bubble, boolean isSuppressed)152         void suppressionChanged(Bubble bubble, boolean isSuppressed);
153         /** Called when the expansion state of bubbles has changed. */
expansionChanged(boolean isExpanded)154         void expansionChanged(boolean isExpanded);
155         /**
156          * Called when the order of the bubble list has changed. Depending on the expanded state
157          * the pointer might need to be updated.
158          */
bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer)159         void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer);
160     }
161 
162     private final Context mContext;
163     private final BubblesImpl mImpl = new BubblesImpl();
164     private Bubbles.BubbleExpandListener mExpandListener;
165     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
166     private final FloatingContentCoordinator mFloatingContentCoordinator;
167     private final BubbleDataRepository mDataRepository;
168     private final WindowManagerShellWrapper mWindowManagerShellWrapper;
169     private final UserManager mUserManager;
170     private final LauncherApps mLauncherApps;
171     private final IStatusBarService mBarService;
172     private final WindowManager mWindowManager;
173     private final TaskStackListenerImpl mTaskStackListener;
174     private final ShellTaskOrganizer mTaskOrganizer;
175     private final DisplayController mDisplayController;
176     private final TaskViewTransitions mTaskViewTransitions;
177     private final SyncTransactionQueue mSyncQueue;
178     private final ShellController mShellController;
179     private final ShellCommandHandler mShellCommandHandler;
180 
181     // Used to post to main UI thread
182     private final ShellExecutor mMainExecutor;
183     private final Handler mMainHandler;
184     private final ShellExecutor mBackgroundExecutor;
185 
186     private BubbleLogger mLogger;
187     private BubbleData mBubbleData;
188     @Nullable private BubbleStackView mStackView;
189     private BubbleIconFactory mBubbleIconFactory;
190     private BubbleBadgeIconFactory mBubbleBadgeIconFactory;
191     private BubblePositioner mBubblePositioner;
192     private Bubbles.SysuiProxy mSysuiProxy;
193 
194     // Tracks the id of the current (foreground) user.
195     private int mCurrentUserId;
196     // Current profiles of the user (e.g. user with a workprofile)
197     private SparseArray<UserInfo> mCurrentProfiles;
198     // Saves data about active bubbles when users are switched.
199     private final SparseArray<UserBubbleData> mSavedUserBubbleData;
200 
201     // Used when ranking updates occur and we check if things should bubble / unbubble
202     private NotificationListenerService.Ranking mTmpRanking;
203 
204     // Callback that updates BubbleOverflowActivity on data change.
205     @Nullable private BubbleData.Listener mOverflowListener = null;
206 
207     // Typically only load once & after user switches
208     private boolean mOverflowDataLoadNeeded = true;
209 
210     /**
211      * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select
212      * this bubble and expand the stack.
213      */
214     @Nullable private BubbleEntry mNotifEntryToExpandOnShadeUnlock;
215 
216     /** LayoutParams used to add the BubbleStackView to the window manager. */
217     private WindowManager.LayoutParams mWmLayoutParams;
218     /** Whether or not the BubbleStackView has been added to the WindowManager. */
219     private boolean mAddedToWindowManager = false;
220 
221     /** Saved screen density, used to detect display size changes in {@link #onConfigChanged}. */
222     private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED;
223 
224     /** Saved screen bounds, used to detect screen size changes in {@link #onConfigChanged}. **/
225     private Rect mScreenBounds = new Rect();
226 
227     /** Saved font scale, used to detect font size changes in {@link #onConfigChanged}. */
228     private float mFontScale = 0;
229 
230     /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */
231     private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
232 
233     /** Saved insets, used to detect WindowInset changes. */
234     private WindowInsets mWindowInsets;
235 
236     private boolean mInflateSynchronously;
237 
238     /** True when user is in status bar unlock shade. */
239     private boolean mIsStatusBarShade = true;
240 
241     /** One handed mode controller to register transition listener. */
242     private Optional<OneHandedController> mOneHandedOptional;
243     /** Drag and drop controller to register listener for onDragStarted. */
244     private DragAndDropController mDragAndDropController;
245 
BubbleController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, BubbleDataRepository dataRepository, @Nullable IStatusBarService statusBarService, WindowManager windowManager, WindowManagerShellWrapper windowManagerShellWrapper, UserManager userManager, LauncherApps launcherApps, BubbleLogger bubbleLogger, TaskStackListenerImpl taskStackListener, ShellTaskOrganizer organizer, BubblePositioner positioner, DisplayController displayController, Optional<OneHandedController> oneHandedOptional, DragAndDropController dragAndDropController, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellBackgroundThread ShellExecutor bgExecutor, TaskViewTransitions taskViewTransitions, SyncTransactionQueue syncQueue)246     public BubbleController(Context context,
247             ShellInit shellInit,
248             ShellCommandHandler shellCommandHandler,
249             ShellController shellController,
250             BubbleData data,
251             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
252             FloatingContentCoordinator floatingContentCoordinator,
253             BubbleDataRepository dataRepository,
254             @Nullable IStatusBarService statusBarService,
255             WindowManager windowManager,
256             WindowManagerShellWrapper windowManagerShellWrapper,
257             UserManager userManager,
258             LauncherApps launcherApps,
259             BubbleLogger bubbleLogger,
260             TaskStackListenerImpl taskStackListener,
261             ShellTaskOrganizer organizer,
262             BubblePositioner positioner,
263             DisplayController displayController,
264             Optional<OneHandedController> oneHandedOptional,
265             DragAndDropController dragAndDropController,
266             @ShellMainThread ShellExecutor mainExecutor,
267             @ShellMainThread Handler mainHandler,
268             @ShellBackgroundThread ShellExecutor bgExecutor,
269             TaskViewTransitions taskViewTransitions,
270             SyncTransactionQueue syncQueue) {
271         mContext = context;
272         mShellCommandHandler = shellCommandHandler;
273         mShellController = shellController;
274         mLauncherApps = launcherApps;
275         mBarService = statusBarService == null
276                 ? IStatusBarService.Stub.asInterface(
277                 ServiceManager.getService(Context.STATUS_BAR_SERVICE))
278                 : statusBarService;
279         mWindowManager = windowManager;
280         mWindowManagerShellWrapper = windowManagerShellWrapper;
281         mUserManager = userManager;
282         mFloatingContentCoordinator = floatingContentCoordinator;
283         mDataRepository = dataRepository;
284         mLogger = bubbleLogger;
285         mMainExecutor = mainExecutor;
286         mMainHandler = mainHandler;
287         mBackgroundExecutor = bgExecutor;
288         mTaskStackListener = taskStackListener;
289         mTaskOrganizer = organizer;
290         mSurfaceSynchronizer = synchronizer;
291         mCurrentUserId = ActivityManager.getCurrentUser();
292         mBubblePositioner = positioner;
293         mBubbleData = data;
294         mSavedUserBubbleData = new SparseArray<>();
295         mBubbleIconFactory = new BubbleIconFactory(context);
296         mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context);
297         mDisplayController = displayController;
298         mTaskViewTransitions = taskViewTransitions;
299         mOneHandedOptional = oneHandedOptional;
300         mDragAndDropController = dragAndDropController;
301         mSyncQueue = syncQueue;
302         shellInit.addInitCallback(this::onInit, this);
303     }
304 
registerOneHandedState(OneHandedController oneHanded)305     private void registerOneHandedState(OneHandedController oneHanded) {
306         oneHanded.registerTransitionCallback(
307                 new OneHandedTransitionCallback() {
308                     @Override
309                     public void onStartFinished(Rect bounds) {
310                         if (mStackView != null) {
311                             mStackView.onVerticalOffsetChanged(bounds.top);
312                         }
313                     }
314 
315                     @Override
316                     public void onStopFinished(Rect bounds) {
317                         if (mStackView != null) {
318                             mStackView.onVerticalOffsetChanged(bounds.top);
319                         }
320                     }
321                 });
322     }
323 
onInit()324     protected void onInit() {
325         mBubbleData.setListener(mBubbleDataListener);
326         mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
327         mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
328 
329         mBubbleData.setPendingIntentCancelledListener(bubble -> {
330             if (bubble.getBubbleIntent() == null) {
331                 return;
332             }
333             if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
334                 bubble.setPendingIntentCanceled();
335                 return;
336             }
337             mMainExecutor.execute(() -> removeBubble(bubble.getKey(), DISMISS_INVALID_INTENT));
338         });
339 
340         try {
341             mWindowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener());
342         } catch (RemoteException e) {
343             e.printStackTrace();
344         }
345 
346         mBubbleData.setCurrentUserId(mCurrentUserId);
347 
348         mTaskOrganizer.addLocusIdListener((taskId, locus, visible) ->
349                 mBubbleData.onLocusVisibilityChanged(taskId, locus, visible));
350 
351         mLauncherApps.registerCallback(new LauncherApps.Callback() {
352             @Override
353             public void onPackageAdded(String s, UserHandle userHandle) {}
354 
355             @Override
356             public void onPackageChanged(String s, UserHandle userHandle) {}
357 
358             @Override
359             public void onPackageRemoved(String s, UserHandle userHandle) {
360                 // Remove bubbles with this package name, since it has been uninstalled and attempts
361                 // to open a bubble from an uninstalled app can cause issues.
362                 mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED);
363             }
364 
365             @Override
366             public void onPackagesAvailable(String[] strings, UserHandle userHandle, boolean b) {}
367 
368             @Override
369             public void onPackagesUnavailable(String[] packages, UserHandle userHandle,
370                     boolean b) {
371                 for (String packageName : packages) {
372                     // Remove bubbles from unavailable apps. This can occur when the app is on
373                     // external storage that has been removed.
374                     mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED);
375                 }
376             }
377 
378             @Override
379             public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts,
380                     UserHandle user) {
381                 super.onShortcutsChanged(packageName, validShortcuts, user);
382 
383                 // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts.
384                 mBubbleData.removeBubblesWithInvalidShortcuts(
385                         packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED);
386             }
387         }, mMainHandler);
388 
389         mTaskStackListener.addListener(new TaskStackListenerCallback() {
390             @Override
391             public void onTaskMovedToFront(int taskId) {
392                 mMainExecutor.execute(() -> {
393                     int expandedId = INVALID_TASK_ID;
394                     if (mStackView != null && mStackView.getExpandedBubble() != null
395                             && isStackExpanded()
396                             && !mStackView.isExpansionAnimating()
397                             && !mStackView.isSwitchAnimating()) {
398                         expandedId = mStackView.getExpandedBubble().getTaskId();
399                     }
400                     if (expandedId != INVALID_TASK_ID && expandedId != taskId) {
401                         mBubbleData.setExpanded(false);
402                     }
403                 });
404             }
405 
406             @Override
407             public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
408                     boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
409                 for (Bubble b : mBubbleData.getBubbles()) {
410                     if (task.taskId == b.getTaskId()) {
411                         mBubbleData.setSelectedBubble(b);
412                         mBubbleData.setExpanded(true);
413                         return;
414                     }
415                 }
416                 for (Bubble b : mBubbleData.getOverflowBubbles()) {
417                     if (task.taskId == b.getTaskId()) {
418                         promoteBubbleFromOverflow(b);
419                         mBubbleData.setExpanded(true);
420                         return;
421                     }
422                 }
423             }
424         });
425 
426         mDisplayController.addDisplayChangingController(
427                 (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> {
428                     // This is triggered right before the rotation is applied
429                     if (fromRotation != toRotation) {
430                         if (mStackView != null) {
431                             // Layout listener set on stackView will update the positioner
432                             // once the rotation is applied
433                             mStackView.onOrientationChanged();
434                         }
435                     }
436                 });
437 
438         mOneHandedOptional.ifPresent(this::registerOneHandedState);
439         mDragAndDropController.addListener(this::collapseStack);
440 
441         // Clear out any persisted bubbles on disk that no longer have a valid user.
442         List<UserInfo> users = mUserManager.getAliveUsers();
443         mDataRepository.sanitizeBubbles(users);
444 
445         // Init profiles
446         SparseArray<UserInfo> userProfiles = new SparseArray<>();
447         for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
448             userProfiles.put(user.id, user);
449         }
450         mCurrentProfiles = userProfiles;
451 
452         mShellController.addConfigurationChangeListener(this);
453         mShellCommandHandler.addDumpCallback(this::dump, this);
454     }
455 
456     @VisibleForTesting
asBubbles()457     public Bubbles asBubbles() {
458         return mImpl;
459     }
460 
461     @VisibleForTesting
getImplCachedState()462     public BubblesImpl.CachedState getImplCachedState() {
463         return mImpl.mCachedState;
464     }
465 
getMainExecutor()466     public ShellExecutor getMainExecutor() {
467         return mMainExecutor;
468     }
469 
470     /**
471      * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
472      */
hideCurrentInputMethod()473     void hideCurrentInputMethod() {
474         try {
475             mBarService.hideCurrentInputMethodForBubbles();
476         } catch (RemoteException e) {
477             e.printStackTrace();
478         }
479     }
480 
openBubbleOverflow()481     private void openBubbleOverflow() {
482         ensureStackViewCreated();
483         mBubbleData.setShowingOverflow(true);
484         mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
485         mBubbleData.setExpanded(true);
486     }
487 
488     /**
489      * Called when the status bar has become visible or invisible (either permanently or
490      * temporarily).
491      */
onStatusBarVisibilityChanged(boolean visible)492     private void onStatusBarVisibilityChanged(boolean visible) {
493         if (mStackView != null) {
494             // Hide the stack temporarily if the status bar has been made invisible, and the stack
495             // is collapsed. An expanded stack should remain visible until collapsed.
496             mStackView.setTemporarilyInvisible(!visible && !isStackExpanded());
497         }
498     }
499 
onZenStateChanged()500     private void onZenStateChanged() {
501         for (Bubble b : mBubbleData.getBubbles()) {
502             b.setShowDot(b.showInShade());
503         }
504     }
505 
506     @VisibleForTesting
onStatusBarStateChanged(boolean isShade)507     public void onStatusBarStateChanged(boolean isShade) {
508         boolean didChange = mIsStatusBarShade != isShade;
509         if (DEBUG_BUBBLE_CONTROLLER) {
510             Log.d(TAG, "onStatusBarStateChanged isShade=" + isShade + " didChange=" + didChange);
511         }
512         mIsStatusBarShade = isShade;
513         if (!mIsStatusBarShade && didChange) {
514             // Only collapse stack on change
515             collapseStack();
516         }
517 
518         if (mNotifEntryToExpandOnShadeUnlock != null) {
519             expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
520         }
521 
522         updateStack();
523     }
524 
525     @VisibleForTesting
onBubbleMetadataFlagChanged(Bubble bubble)526     public void onBubbleMetadataFlagChanged(Bubble bubble) {
527         // Make sure NoMan knows suppression state so that anyone querying it can tell.
528         try {
529             mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags());
530         } catch (RemoteException e) {
531             // Bad things have happened
532         }
533         mImpl.mCachedState.updateBubbleSuppressedState(bubble);
534     }
535 
536     /** Called when the current user changes. */
537     @VisibleForTesting
onUserChanged(int newUserId)538     public void onUserChanged(int newUserId) {
539         saveBubbles(mCurrentUserId);
540         mCurrentUserId = newUserId;
541 
542         mBubbleData.dismissAll(DISMISS_USER_CHANGED);
543         mBubbleData.clearOverflow();
544         mOverflowDataLoadNeeded = true;
545 
546         restoreBubbles(newUserId);
547         mBubbleData.setCurrentUserId(newUserId);
548     }
549 
550     /** Called when the profiles for the current user change. **/
onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)551     public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
552         mCurrentProfiles = currentProfiles;
553     }
554 
555     /** Called when a user is removed from the device, including work profiles. */
onUserRemoved(int removedUserId)556     public void onUserRemoved(int removedUserId) {
557         UserInfo parent = mUserManager.getProfileParent(removedUserId);
558         int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1;
559         mBubbleData.removeBubblesForUser(removedUserId);
560         // Typically calls from BubbleData would remove bubbles from the DataRepository as well,
561         // however, this gets complicated when users are removed (mCurrentUserId won't necessarily
562         // be correct for this) so we update the repo directly.
563         mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
564     }
565 
566     /** Whether bubbles are showing in the bubble bar. */
isShowingAsBubbleBar()567     public boolean isShowingAsBubbleBar() {
568         // TODO(b/269670598): should also check that we're in gesture nav
569         return BUBBLE_BAR_ENABLED && mBubblePositioner.isLargeScreen();
570     }
571 
572     /** Whether this userId belongs to the current user. */
isCurrentProfile(int userId)573     private boolean isCurrentProfile(int userId) {
574         return userId == UserHandle.USER_ALL
575                 || (mCurrentProfiles != null && mCurrentProfiles.get(userId) != null);
576     }
577 
578     /**
579      * Sets whether to perform inflation on the same thread as the caller. This method should only
580      * be used in tests, not in production.
581      */
582     @VisibleForTesting
setInflateSynchronously(boolean inflateSynchronously)583     public void setInflateSynchronously(boolean inflateSynchronously) {
584         mInflateSynchronously = inflateSynchronously;
585     }
586 
587     /** Set a listener to be notified of when overflow view update. */
setOverflowListener(BubbleData.Listener listener)588     public void setOverflowListener(BubbleData.Listener listener) {
589         mOverflowListener = listener;
590     }
591 
592     /**
593      * @return Bubbles for updating overflow.
594      */
getOverflowBubbles()595     List<Bubble> getOverflowBubbles() {
596         return mBubbleData.getOverflowBubbles();
597     }
598 
599     /** The task listener for events in bubble tasks. */
getTaskOrganizer()600     public ShellTaskOrganizer getTaskOrganizer() {
601         return mTaskOrganizer;
602     }
603 
getSyncTransactionQueue()604     SyncTransactionQueue getSyncTransactionQueue() {
605         return mSyncQueue;
606     }
607 
getTaskViewTransitions()608     TaskViewTransitions getTaskViewTransitions() {
609         return mTaskViewTransitions;
610     }
611 
612     /** Contains information to help position things on the screen. */
613     @VisibleForTesting
getPositioner()614     public BubblePositioner getPositioner() {
615         return mBubblePositioner;
616     }
617 
getSysuiProxy()618     Bubbles.SysuiProxy getSysuiProxy() {
619         return mSysuiProxy;
620     }
621 
622     /**
623      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
624      * method initializes the stack view and adds it to window manager.
625      */
ensureStackViewCreated()626     private void ensureStackViewCreated() {
627         if (mStackView == null) {
628             mStackView = new BubbleStackView(
629                     mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
630                     mMainExecutor);
631             mStackView.onOrientationChanged();
632             if (mExpandListener != null) {
633                 mStackView.setExpandListener(mExpandListener);
634             }
635             mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
636         }
637 
638         addToWindowManagerMaybe();
639     }
640 
641     /** Adds the BubbleStackView to the WindowManager if it's not already there. */
addToWindowManagerMaybe()642     private void addToWindowManagerMaybe() {
643         // If the stack is null, or already added, don't add it.
644         if (mStackView == null || mAddedToWindowManager) {
645             return;
646         }
647 
648         mWmLayoutParams = new WindowManager.LayoutParams(
649                 // Fill the screen so we can use translation animations to position the bubble
650                 // stack. We'll use touchable regions to ignore touches that are not on the bubbles
651                 // themselves.
652                 ViewGroup.LayoutParams.MATCH_PARENT,
653                 ViewGroup.LayoutParams.MATCH_PARENT,
654                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
655                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
656                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
657                         | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
658                 PixelFormat.TRANSLUCENT);
659 
660         mWmLayoutParams.setTrustedOverlay();
661         mWmLayoutParams.setFitInsetsTypes(0);
662         mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
663         mWmLayoutParams.token = new Binder();
664         mWmLayoutParams.setTitle("Bubbles!");
665         mWmLayoutParams.packageName = mContext.getPackageName();
666         mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
667         mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
668 
669         try {
670             mAddedToWindowManager = true;
671             registerBroadcastReceiver();
672             mBubbleData.getOverflow().initialize(this);
673             mWindowManager.addView(mStackView, mWmLayoutParams);
674             mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
675                 if (!windowInsets.equals(mWindowInsets)) {
676                     mWindowInsets = windowInsets;
677                     mBubblePositioner.update();
678                     mStackView.onDisplaySizeChanged();
679                 }
680                 return windowInsets;
681             });
682         } catch (IllegalStateException e) {
683             // This means the stack has already been added. This shouldn't happen...
684             e.printStackTrace();
685         }
686     }
687 
688     /**
689      * In some situations bubble's should be able to receive key events for back:
690      * - when the bubble overflow is showing
691      * - when the user education for the stack is showing.
692      *
693      * @param interceptBack whether back should be intercepted or not.
694      */
updateWindowFlagsForBackpress(boolean interceptBack)695     void updateWindowFlagsForBackpress(boolean interceptBack) {
696         if (mStackView != null && mAddedToWindowManager) {
697             mWmLayoutParams.flags = interceptBack
698                     ? 0
699                     : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
700                             | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
701             mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
702             mWindowManager.updateViewLayout(mStackView, mWmLayoutParams);
703         }
704     }
705 
706     /** Removes the BubbleStackView from the WindowManager if it's there. */
removeFromWindowManagerMaybe()707     private void removeFromWindowManagerMaybe() {
708         if (!mAddedToWindowManager) {
709             return;
710         }
711 
712         try {
713             mAddedToWindowManager = false;
714             // Put on background for this binder call, was causing jank
715             mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver));
716             if (mStackView != null) {
717                 mWindowManager.removeView(mStackView);
718                 mBubbleData.getOverflow().cleanUpExpandedState();
719             } else {
720                 Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
721             }
722         } catch (IllegalArgumentException e) {
723             // This means the stack has already been removed - it shouldn't happen, but ignore if it
724             // does, since we wanted it removed anyway.
725             e.printStackTrace();
726         }
727     }
728 
registerBroadcastReceiver()729     private void registerBroadcastReceiver() {
730         IntentFilter filter = new IntentFilter();
731         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
732         filter.addAction(Intent.ACTION_SCREEN_OFF);
733         mContext.registerReceiver(mBroadcastReceiver, filter);
734     }
735 
736     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
737         @Override
738         public void onReceive(Context context, Intent intent) {
739             if (!isStackExpanded()) return; // Nothing to do
740 
741             String action = intent.getAction();
742             String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
743             if ((Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)
744                     && SYSTEM_DIALOG_REASON_GESTURE_NAV.equals(reason))
745                     || Intent.ACTION_SCREEN_OFF.equals(action)) {
746                 mMainExecutor.execute(() -> collapseStack());
747             }
748         }
749     };
750 
751     /**
752      * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
753      * added in the meantime.
754      */
755     @VisibleForTesting
onAllBubblesAnimatedOut()756     public void onAllBubblesAnimatedOut() {
757         if (mStackView != null) {
758             mStackView.setVisibility(INVISIBLE);
759             removeFromWindowManagerMaybe();
760         }
761     }
762 
763     /**
764      * Records the notification key for any active bubbles. These are used to restore active
765      * bubbles when the user returns to the foreground.
766      *
767      * @param userId the id of the user
768      */
saveBubbles(@serIdInt int userId)769     private void saveBubbles(@UserIdInt int userId) {
770         // First clear any existing keys that might be stored.
771         mSavedUserBubbleData.remove(userId);
772         UserBubbleData userBubbleData = new UserBubbleData();
773         // Add in all active bubbles for the current user.
774         for (Bubble bubble : mBubbleData.getBubbles()) {
775             userBubbleData.add(bubble.getKey(), bubble.showInShade());
776         }
777         mSavedUserBubbleData.put(userId, userBubbleData);
778     }
779 
780     /**
781      * Promotes existing notifications to Bubbles if they were previously bubbles.
782      *
783      * @param userId the id of the user
784      */
restoreBubbles(@serIdInt int userId)785     private void restoreBubbles(@UserIdInt int userId) {
786         UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId);
787         if (savedBubbleData == null) {
788             // There were no bubbles saved for this used.
789             return;
790         }
791         mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> {
792             mMainExecutor.execute(() -> {
793                 for (BubbleEntry e : entries) {
794                     if (canLaunchInTaskView(mContext, e)) {
795                         boolean showInShade = savedBubbleData.isShownInShade(e.getKey());
796                         updateBubble(e, true /* suppressFlyout */, showInShade);
797                     }
798                 }
799             });
800         });
801         // Finally, remove the entries for this user now that bubbles are restored.
802         mSavedUserBubbleData.remove(userId);
803     }
804 
805     @Override
onThemeChanged()806     public void onThemeChanged() {
807         if (mStackView != null) {
808             mStackView.onThemeChanged();
809         }
810         mBubbleIconFactory = new BubbleIconFactory(mContext);
811         mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext);
812 
813         // Reload each bubble
814         for (Bubble b : mBubbleData.getBubbles()) {
815             b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
816                     mBubbleBadgeIconFactory,
817                     false /* skipInflation */);
818         }
819         for (Bubble b : mBubbleData.getOverflowBubbles()) {
820             b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
821                     mBubbleBadgeIconFactory,
822                     false /* skipInflation */);
823         }
824     }
825 
826     @Override
onConfigurationChanged(Configuration newConfig)827     public void onConfigurationChanged(Configuration newConfig) {
828         if (mBubblePositioner != null) {
829             mBubblePositioner.update();
830         }
831         if (mStackView != null && newConfig != null) {
832             if (newConfig.densityDpi != mDensityDpi
833                     || !newConfig.windowConfiguration.getBounds().equals(mScreenBounds)) {
834                 mDensityDpi = newConfig.densityDpi;
835                 mScreenBounds.set(newConfig.windowConfiguration.getBounds());
836                 mBubbleData.onMaxBubblesChanged();
837                 mBubbleIconFactory = new BubbleIconFactory(mContext);
838                 mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(mContext);
839                 mStackView.onDisplaySizeChanged();
840             }
841             if (newConfig.fontScale != mFontScale) {
842                 mFontScale = newConfig.fontScale;
843                 mStackView.updateFontScale();
844             }
845             if (newConfig.getLayoutDirection() != mLayoutDirection) {
846                 mLayoutDirection = newConfig.getLayoutDirection();
847                 mStackView.onLayoutDirectionChanged(mLayoutDirection);
848             }
849         }
850     }
851 
onNotificationPanelExpandedChanged(boolean expanded)852     private void onNotificationPanelExpandedChanged(boolean expanded) {
853         if (DEBUG_BUBBLE_GESTURE) {
854             Log.d(TAG, "onNotificationPanelExpandedChanged: expanded=" + expanded);
855         }
856         if (mStackView != null && mStackView.isExpanded()) {
857             if (expanded) {
858                 mStackView.stopMonitoringSwipeUpGesture();
859             } else {
860                 mStackView.startMonitoringSwipeUpGesture();
861             }
862         }
863     }
864 
setSysuiProxy(Bubbles.SysuiProxy proxy)865     private void setSysuiProxy(Bubbles.SysuiProxy proxy) {
866         mSysuiProxy = proxy;
867     }
868 
869     @VisibleForTesting
setExpandListener(Bubbles.BubbleExpandListener listener)870     public void setExpandListener(Bubbles.BubbleExpandListener listener) {
871         mExpandListener = ((isExpanding, key) -> {
872             if (listener != null) {
873                 listener.onBubbleExpandChanged(isExpanding, key);
874             }
875         });
876         if (mStackView != null) {
877             mStackView.setExpandListener(mExpandListener);
878         }
879     }
880 
881     /**
882      * Whether or not there are bubbles present, regardless of them being visible on the
883      * screen (e.g. if on AOD).
884      */
885     @VisibleForTesting
hasBubbles()886     public boolean hasBubbles() {
887         if (mStackView == null) {
888             return false;
889         }
890         return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
891     }
892 
893     @VisibleForTesting
isStackExpanded()894     public boolean isStackExpanded() {
895         return mBubbleData.isExpanded();
896     }
897 
collapseStack()898     public void collapseStack() {
899         mBubbleData.setExpanded(false /* expanded */);
900     }
901 
902     @VisibleForTesting
isBubbleNotificationSuppressedFromShade(String key, String groupKey)903     public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
904         boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
905                 && !mBubbleData.getAnyBubbleWithkey(key).showInShade());
906 
907         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
908         boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey));
909         return (isSummary && isSuppressedSummary) || isSuppressedBubble;
910     }
911 
912     /** Promote the provided bubble from the overflow view. */
promoteBubbleFromOverflow(Bubble bubble)913     public void promoteBubbleFromOverflow(Bubble bubble) {
914         mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
915         bubble.setInflateSynchronously(mInflateSynchronously);
916         bubble.setShouldAutoExpand(true);
917         bubble.markAsAccessedAt(System.currentTimeMillis());
918         setIsBubble(bubble, true /* isBubble */);
919     }
920 
921     /**
922      * Expands and selects the provided bubble as long as it already exists in the stack or the
923      * overflow.
924      *
925      * This is currently only used when opening a bubble via clicking on a conversation widget.
926      */
expandStackAndSelectBubble(Bubble b)927     public void expandStackAndSelectBubble(Bubble b) {
928         if (b == null) {
929             return;
930         }
931         if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) {
932             // already in the stack
933             mBubbleData.setSelectedBubble(b);
934             mBubbleData.setExpanded(true);
935         } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) {
936             // promote it out of the overflow
937             promoteBubbleFromOverflow(b);
938         }
939     }
940 
941     /**
942      * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble
943      * exists for this entry, and it is able to bubble, a new bubble will be created.
944      *
945      * This is the method to use when opening a bubble via a notification or in a state where
946      * the device might not be unlocked.
947      *
948      * @param entry the entry to use for the bubble.
949      */
expandStackAndSelectBubble(BubbleEntry entry)950     public void expandStackAndSelectBubble(BubbleEntry entry) {
951         if (mIsStatusBarShade) {
952             mNotifEntryToExpandOnShadeUnlock = null;
953 
954             String key = entry.getKey();
955             Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
956             if (bubble != null) {
957                 mBubbleData.setSelectedBubble(bubble);
958                 mBubbleData.setExpanded(true);
959             } else {
960                 bubble = mBubbleData.getOverflowBubbleWithKey(key);
961                 if (bubble != null) {
962                     promoteBubbleFromOverflow(bubble);
963                 } else if (entry.canBubble()) {
964                     // It can bubble but it's not -- it got aged out of the overflow before it
965                     // was dismissed or opened, make it a bubble again.
966                     setIsBubble(entry, true /* isBubble */, true /* autoExpand */);
967                 }
968             }
969         } else {
970             // Wait until we're unlocked to expand, so that the user can see the expand animation
971             // and also to work around bugs with expansion animation + shade unlock happening at the
972             // same time.
973             mNotifEntryToExpandOnShadeUnlock = entry;
974         }
975     }
976 
977     /**
978      * Adds or updates a bubble associated with the provided notification entry.
979      *
980      * @param notif the notification associated with this bubble.
981      */
982     @VisibleForTesting
updateBubble(BubbleEntry notif)983     public void updateBubble(BubbleEntry notif) {
984         int bubbleUserId = notif.getStatusBarNotification().getUserId();
985         if (isCurrentProfile(bubbleUserId)) {
986             updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
987         } else {
988             // Skip update, but store it in user bubbles so it gets restored after user switch
989             mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(),
990                     true /* shownInShade */);
991             if (DEBUG_BUBBLE_CONTROLLER) {
992                 Log.d(TAG,
993                         "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId
994                                 + " current userId=" + mCurrentUserId);
995             }
996         }
997     }
998 
999     /**
1000      * This method has different behavior depending on:
1001      *    - if an app bubble exists
1002      *    - if an app bubble is expanded
1003      *
1004      * If no app bubble exists, this will add and expand a bubble with the provided intent. The
1005      * intent must be explicit (i.e. include a package name or fully qualified component class name)
1006      * and the activity for it should be resizable.
1007      *
1008      * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
1009      * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
1010      * this method will expand it.
1011      *
1012      * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
1013      * the bubble or bubble stack.
1014      *
1015      * Some notes:
1016      *    - Only one app bubble is supported at a time
1017      *    - Calling this method with a different intent than the existing app bubble will do nothing
1018      *
1019      * @param intent the intent to display in the bubble expanded view.
1020      */
showOrHideAppBubble(Intent intent)1021     public void showOrHideAppBubble(Intent intent) {
1022         if (intent == null || intent.getPackage() == null) {
1023             Log.w(TAG, "App bubble failed to show, invalid intent: " + intent
1024                     + ((intent != null) ? " with package: " + intent.getPackage() : " "));
1025             return;
1026         }
1027 
1028         PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId);
1029         if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return;
1030 
1031         Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE);
1032         if (existingAppBubble != null) {
1033             BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
1034             if (isStackExpanded()) {
1035                 if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) {
1036                     // App bubble is expanded, lets collapse
1037                     collapseStack();
1038                 } else {
1039                     // App bubble is not selected, select it
1040                     mBubbleData.setSelectedBubble(existingAppBubble);
1041                 }
1042             } else {
1043                 // App bubble is not selected, select it & expand
1044                 mBubbleData.setSelectedBubble(existingAppBubble);
1045                 mBubbleData.setExpanded(true);
1046             }
1047         } else {
1048             // App bubble does not exist, lets add and expand it
1049             Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
1050             b.setShouldAutoExpand(true);
1051             inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
1052         }
1053     }
1054 
1055     /**
1056      * Fills the overflow bubbles by loading them from disk.
1057      */
loadOverflowBubblesFromDisk()1058     void loadOverflowBubblesFromDisk() {
1059         if (!mOverflowDataLoadNeeded) {
1060             return;
1061         }
1062         mOverflowDataLoadNeeded = false;
1063         mDataRepository.loadBubbles(mCurrentUserId, (bubbles) -> {
1064             bubbles.forEach(bubble -> {
1065                 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) {
1066                     // if the bubble is already active, there's no need to push it to overflow
1067                     return;
1068                 }
1069                 bubble.inflate(
1070                         (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble),
1071                         mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
1072                         true /* skipInflation */);
1073             });
1074             return null;
1075         });
1076     }
1077 
1078     /**
1079      * Adds or updates a bubble associated with the provided notification entry.
1080      *
1081      * @param notif          the notification associated with this bubble.
1082      * @param suppressFlyout this bubble suppress flyout or not.
1083      * @param showInShade    this bubble show in shade or not.
1084      */
1085     @VisibleForTesting
updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade)1086     public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) {
1087         // If this is an interruptive notif, mark that it's interrupted
1088         mSysuiProxy.setNotificationInterruption(notif.getKey());
1089         boolean isNonInterruptiveNotExpanding = !notif.getRanking().isTextChanged()
1090                 && (notif.getBubbleMetadata() != null
1091                 && !notif.getBubbleMetadata().getAutoExpandBubble());
1092         if (isNonInterruptiveNotExpanding
1093                 && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
1094             // Update the bubble but don't promote it out of overflow
1095             Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey());
1096             if (notif.isBubble()) {
1097                 notif.setFlagBubble(false);
1098             }
1099             updateNotNotifyingEntry(b, notif, showInShade);
1100         } else if (mBubbleData.hasAnyBubbleWithKey(notif.getKey())
1101                 && isNonInterruptiveNotExpanding) {
1102             Bubble b = mBubbleData.getAnyBubbleWithkey(notif.getKey());
1103             if (b != null) {
1104                 updateNotNotifyingEntry(b, notif, showInShade);
1105             }
1106         } else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) {
1107             // Update the bubble but don't promote it out of overflow
1108             Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey());
1109             if (b != null) {
1110                 updateNotNotifyingEntry(b, notif, showInShade);
1111             }
1112         } else {
1113             Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
1114             if (notif.shouldSuppressNotificationList()) {
1115                 // If we're suppressing notifs for DND, we don't want the bubbles to randomly
1116                 // expand when DND turns off so flip the flag.
1117                 if (bubble.shouldAutoExpand()) {
1118                     bubble.setShouldAutoExpand(false);
1119                 }
1120                 mImpl.mCachedState.updateBubbleSuppressedState(bubble);
1121             } else {
1122                 inflateAndAdd(bubble, suppressFlyout, showInShade);
1123             }
1124         }
1125     }
1126 
updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade)1127     void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) {
1128         boolean showInShadeBefore = b.showInShade();
1129         boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble());
1130         boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected;
1131         b.setEntry(entry);
1132         boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade();
1133         b.setSuppressNotification(suppress);
1134         b.setShowDot(!isBubbleExpandedAndSelected);
1135         if (showInShadeBefore != b.showInShade()) {
1136             mImpl.mCachedState.updateBubbleSuppressedState(b);
1137         }
1138     }
1139 
1140     @VisibleForTesting
inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade)1141     public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
1142         // Lazy init stack view when a bubble is created
1143         ensureStackViewCreated();
1144         bubble.setInflateSynchronously(mInflateSynchronously);
1145         bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
1146                 mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
1147                 false /* skipInflation */);
1148     }
1149 
1150     /**
1151      * Removes the bubble with the given key.
1152      * <p>
1153      * Must be called from the main thread.
1154      */
1155     @VisibleForTesting
1156     @MainThread
removeBubble(String key, int reason)1157     public void removeBubble(String key, int reason) {
1158         if (mBubbleData.hasAnyBubbleWithKey(key)) {
1159             mBubbleData.dismissBubbleWithKey(key, reason);
1160         }
1161     }
1162 
onEntryAdded(BubbleEntry entry)1163     private void onEntryAdded(BubbleEntry entry) {
1164         if (canLaunchInTaskView(mContext, entry)) {
1165             updateBubble(entry);
1166         }
1167     }
1168 
1169     @VisibleForTesting
onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem)1170     public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) {
1171         if (!fromSystem) {
1172             return;
1173         }
1174         // shouldBubbleUp checks canBubble & for bubble metadata
1175         boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry);
1176         if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) {
1177             // It was previously a bubble but no longer a bubble -- lets remove it
1178             removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
1179         } else if (shouldBubble && entry.isBubble()) {
1180             updateBubble(entry);
1181         }
1182     }
1183 
onEntryRemoved(BubbleEntry entry)1184     private void onEntryRemoved(BubbleEntry entry) {
1185         if (isSummaryOfBubbles(entry)) {
1186             final String groupKey = entry.getStatusBarNotification().getGroupKey();
1187             mBubbleData.removeSuppressedSummary(groupKey);
1188 
1189             // Remove any associated bubble children with the summary
1190             final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey);
1191             for (int i = 0; i < bubbleChildren.size(); i++) {
1192                 removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED);
1193             }
1194         } else {
1195             removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL);
1196         }
1197     }
1198 
1199     @VisibleForTesting
onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)1200     public void onRankingUpdated(RankingMap rankingMap,
1201             HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
1202         if (mTmpRanking == null) {
1203             mTmpRanking = new NotificationListenerService.Ranking();
1204         }
1205         String[] orderedKeys = rankingMap.getOrderedKeys();
1206         for (int i = 0; i < orderedKeys.length; i++) {
1207             String key = orderedKeys[i];
1208             Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key);
1209             BubbleEntry entry = entryData.first;
1210             boolean shouldBubbleUp = entryData.second;
1211             if (entry != null && !isCurrentProfile(
1212                     entry.getStatusBarNotification().getUser().getIdentifier())) {
1213                 return;
1214             }
1215             if (entry != null && (entry.shouldSuppressNotificationList()
1216                     || entry.getRanking().isSuspended())) {
1217                 shouldBubbleUp = false;
1218             }
1219             rankingMap.getRanking(key, mTmpRanking);
1220             boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key);
1221             boolean isActive = mBubbleData.hasBubbleInStackWithKey(key);
1222             if (isActiveOrInOverflow && !mTmpRanking.canBubble()) {
1223                 // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason.
1224                 // This means that the app or channel's ability to bubble has been revoked.
1225                 mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED);
1226             } else if (isActiveOrInOverflow && !shouldBubbleUp) {
1227                 // If this entry is allowed to bubble, but cannot currently bubble up or is
1228                 // suspended, dismiss it. This happens when DND is enabled and configured to hide
1229                 // bubbles, or focus mode is enabled and the app is designated as distracting.
1230                 // Dismissing with the reason DISMISS_NO_BUBBLE_UP will retain the underlying
1231                 // notification, so that the bubble will be re-created if shouldBubbleUp returns
1232                 // true.
1233                 mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP);
1234             } else if (entry != null && mTmpRanking.isBubble() && !isActiveOrInOverflow) {
1235                 entry.setFlagBubble(true);
1236                 onEntryUpdated(entry, shouldBubbleUp, /* fromSystem= */ true);
1237             }
1238         }
1239     }
1240 
1241     @VisibleForTesting
onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)1242     public void onNotificationChannelModified(String pkg, UserHandle user,
1243             NotificationChannel channel, int modificationType) {
1244         // Only query overflow bubbles here because active bubbles will have an active notification
1245         // and channel changes we care about would result in a ranking update.
1246         List<Bubble> overflowBubbles = new ArrayList<>(mBubbleData.getOverflowBubbles());
1247         for (int i = 0; i < overflowBubbles.size(); i++) {
1248             Bubble b = overflowBubbles.get(i);
1249             if (Objects.equals(b.getShortcutId(), channel.getConversationId())
1250                     && b.getPackageName().equals(pkg)
1251                     && b.getUser().getIdentifier() == user.getIdentifier()) {
1252                 if (!channel.canBubble() || channel.isDeleted()) {
1253                     mBubbleData.dismissBubbleWithKey(b.getKey(), DISMISS_NO_LONGER_BUBBLE);
1254                 }
1255             }
1256         }
1257     }
1258 
1259     /**
1260      * Retrieves any bubbles that are part of the notification group represented by the provided
1261      * group key.
1262      */
getBubblesInGroup(@ullable String groupKey)1263     private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) {
1264         ArrayList<Bubble> bubbleChildren = new ArrayList<>();
1265         if (groupKey == null) {
1266             return bubbleChildren;
1267         }
1268         for (Bubble bubble : mBubbleData.getActiveBubbles()) {
1269             if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) {
1270                 bubbleChildren.add(bubble);
1271             }
1272         }
1273         return bubbleChildren;
1274     }
1275 
setIsBubble(@onNull final BubbleEntry entry, final boolean isBubble, final boolean autoExpand)1276     private void setIsBubble(@NonNull final BubbleEntry entry, final boolean isBubble,
1277             final boolean autoExpand) {
1278         Objects.requireNonNull(entry);
1279         entry.setFlagBubble(isBubble);
1280         try {
1281             int flags = 0;
1282             if (autoExpand) {
1283                 flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
1284                 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE;
1285             }
1286             mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags);
1287         } catch (RemoteException e) {
1288             // Bad things have happened
1289         }
1290     }
1291 
setIsBubble(@onNull final Bubble b, final boolean isBubble)1292     private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) {
1293         Objects.requireNonNull(b);
1294         b.setIsBubble(isBubble);
1295         mSysuiProxy.getPendingOrActiveEntry(b.getKey(), (entry) -> {
1296             mMainExecutor.execute(() -> {
1297                 if (entry != null) {
1298                     // Updating the entry to be a bubble will trigger our normal update flow
1299                     setIsBubble(entry, isBubble, b.shouldAutoExpand());
1300                 } else if (isBubble) {
1301                     // If bubble doesn't exist, it's a persisted bubble so we need to add it to the
1302                     // stack ourselves
1303                     Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */);
1304                     inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */,
1305                             !bubble.shouldAutoExpand() /* showInShade */);
1306                 }
1307             });
1308         });
1309     }
1310 
1311     private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() {
1312         @Override
1313         public void removeBubble(Bubble removedBubble) {
1314             if (mStackView != null) {
1315                 mStackView.removeBubble(removedBubble);
1316             }
1317         }
1318 
1319         @Override
1320         public void addBubble(Bubble addedBubble) {
1321             if (mStackView != null) {
1322                 mStackView.addBubble(addedBubble);
1323             }
1324         }
1325 
1326         @Override
1327         public void updateBubble(Bubble updatedBubble) {
1328             if (mStackView != null) {
1329                 mStackView.updateBubble(updatedBubble);
1330             }
1331         }
1332 
1333         @Override
1334         public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) {
1335             if (mStackView != null) {
1336                 mStackView.updateBubbleOrder(bubbleOrder, updatePointer);
1337             }
1338         }
1339 
1340         @Override
1341         public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
1342             if (mStackView != null) {
1343                 mStackView.setBubbleSuppressed(bubble, isSuppressed);
1344             }
1345         }
1346 
1347         @Override
1348         public void expansionChanged(boolean isExpanded) {
1349             if (mStackView != null) {
1350                 mStackView.setExpanded(isExpanded);
1351             }
1352         }
1353 
1354         @Override
1355         public void selectionChanged(BubbleViewProvider selectedBubble) {
1356             if (mStackView != null) {
1357                 mStackView.setSelectedBubble(selectedBubble);
1358             }
1359 
1360         }
1361     };
1362 
1363     @SuppressWarnings("FieldCanBeLocal")
1364     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
1365 
1366         @Override
1367         public void applyUpdate(BubbleData.Update update) {
1368             if (DEBUG_BUBBLE_CONTROLLER) {
1369                 Log.d(TAG, "applyUpdate:" + " bubbleAdded=" + (update.addedBubble != null)
1370                         + " bubbleRemoved="
1371                         + (update.removedBubbles != null && update.removedBubbles.size() > 0)
1372                         + " bubbleUpdated=" + (update.updatedBubble != null)
1373                         + " orderChanged=" + update.orderChanged
1374                         + " expandedChanged=" + update.expandedChanged
1375                         + " selectionChanged=" + update.selectionChanged
1376                         + " suppressed=" + (update.suppressedBubble != null)
1377                         + " unsuppressed=" + (update.unsuppressedBubble != null));
1378             }
1379 
1380             ensureStackViewCreated();
1381 
1382             // Lazy load overflow bubbles from disk
1383             loadOverflowBubblesFromDisk();
1384 
1385             // If bubbles in the overflow have a dot, make sure the overflow shows a dot
1386             updateOverflowButtonDot();
1387 
1388             // Update bubbles in overflow.
1389             if (mOverflowListener != null) {
1390                 mOverflowListener.applyUpdate(update);
1391             }
1392 
1393             // Do removals, if any.
1394             ArrayList<Pair<Bubble, Integer>> removedBubbles =
1395                     new ArrayList<>(update.removedBubbles);
1396             ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>();
1397             for (Pair<Bubble, Integer> removed : removedBubbles) {
1398                 final Bubble bubble = removed.first;
1399                 @Bubbles.DismissReason final int reason = removed.second;
1400 
1401                 mBubbleViewCallback.removeBubble(bubble);
1402 
1403                 // Leave the notification in place if we're dismissing due to user switching, or
1404                 // because DND is suppressing the bubble. In both of those cases, we need to be able
1405                 // to restore the bubble from the notification later.
1406                 if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) {
1407                     continue;
1408                 }
1409                 if (reason == DISMISS_NOTIF_CANCEL
1410                         || reason == DISMISS_SHORTCUT_REMOVED) {
1411                     bubblesToBeRemovedFromRepository.add(bubble);
1412                 }
1413                 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
1414                     if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())
1415                             && (!bubble.showInShade()
1416                             || reason == DISMISS_NOTIF_CANCEL
1417                             || reason == DISMISS_GROUP_CANCELLED)) {
1418                         // The bubble is now gone & the notification is hidden from the shade, so
1419                         // time to actually remove it
1420                         mSysuiProxy.notifyRemoveNotification(bubble.getKey(), REASON_CANCEL);
1421                     } else {
1422                         if (bubble.isBubble()) {
1423                             setIsBubble(bubble, false /* isBubble */);
1424                         }
1425                         mSysuiProxy.updateNotificationBubbleButton(bubble.getKey());
1426                     }
1427                 }
1428             }
1429             mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository);
1430 
1431             if (update.addedBubble != null) {
1432                 mDataRepository.addBubble(mCurrentUserId, update.addedBubble);
1433                 mBubbleViewCallback.addBubble(update.addedBubble);
1434             }
1435 
1436             if (update.updatedBubble != null) {
1437                 mBubbleViewCallback.updateBubble(update.updatedBubble);
1438             }
1439 
1440             if (update.suppressedBubble != null) {
1441                 mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true);
1442             }
1443 
1444             if (update.unsuppressedBubble != null) {
1445                 mBubbleViewCallback.suppressionChanged(update.unsuppressedBubble, false);
1446             }
1447 
1448             boolean collapseStack = update.expandedChanged && !update.expanded;
1449 
1450             // At this point, the correct bubbles are inflated in the stack.
1451             // Make sure the order in bubble data is reflected in bubble row.
1452             if (update.orderChanged) {
1453                 mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
1454                 // if the stack is going to be collapsed, do not update pointer position
1455                 // after reordering
1456                 mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack);
1457             }
1458 
1459             if (collapseStack) {
1460                 mBubbleViewCallback.expansionChanged(/* expanded= */ false);
1461                 mSysuiProxy.requestNotificationShadeTopUi(false, TAG);
1462             }
1463 
1464             if (update.selectionChanged) {
1465                 mBubbleViewCallback.selectionChanged(update.selectedBubble);
1466             }
1467 
1468             // Expanding? Apply this last.
1469             if (update.expandedChanged && update.expanded) {
1470                 mBubbleViewCallback.expansionChanged(/* expanded= */ true);
1471                 mSysuiProxy.requestNotificationShadeTopUi(true, TAG);
1472             }
1473 
1474             mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate");
1475             updateStack();
1476 
1477             // Update the cached state for queries from SysUI
1478             mImpl.mCachedState.update(update);
1479         }
1480     };
1481 
updateOverflowButtonDot()1482     private void updateOverflowButtonDot() {
1483         BubbleOverflow overflow = mBubbleData.getOverflow();
1484         if (overflow == null) return;
1485 
1486         for (Bubble b : mBubbleData.getOverflowBubbles()) {
1487             if (b.showDot()) {
1488                 overflow.setShowDot(true);
1489                 return;
1490             }
1491         }
1492         overflow.setShowDot(false);
1493     }
1494 
handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)1495     private boolean handleDismissalInterception(BubbleEntry entry,
1496             @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
1497         if (isSummaryOfBubbles(entry)) {
1498             handleSummaryDismissalInterception(entry, children, removeCallback);
1499         } else {
1500             Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey());
1501             if (bubble == null || !entry.isBubble()) {
1502                 bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey());
1503             }
1504             if (bubble == null) {
1505                 return false;
1506             }
1507             bubble.setSuppressNotification(true);
1508             bubble.setShowDot(false /* show */);
1509         }
1510         // Update the shade
1511         mSysuiProxy.notifyInvalidateNotifications("BubbleController.handleDismissalInterception");
1512         return true;
1513     }
1514 
isSummaryOfBubbles(BubbleEntry entry)1515     private boolean isSummaryOfBubbles(BubbleEntry entry) {
1516         String groupKey = entry.getStatusBarNotification().getGroupKey();
1517         ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey);
1518         boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey)
1519                 && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey());
1520         boolean isSummary = entry.getStatusBarNotification().getNotification().isGroupSummary();
1521         return (isSuppressedSummary || isSummary) && !bubbleChildren.isEmpty();
1522     }
1523 
handleSummaryDismissalInterception( BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)1524     private void handleSummaryDismissalInterception(
1525             BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) {
1526         if (children != null) {
1527             for (int i = 0; i < children.size(); i++) {
1528                 BubbleEntry child = children.get(i);
1529                 if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) {
1530                     // Suppress the bubbled child
1531                     // As far as group manager is concerned, once a child is no longer shown
1532                     // in the shade, it is essentially removed.
1533                     Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey());
1534                     if (bubbleChild != null) {
1535                         bubbleChild.setSuppressNotification(true);
1536                         bubbleChild.setShowDot(false /* show */);
1537                     }
1538                 } else {
1539                     // non-bubbled children can be removed
1540                     removeCallback.accept(i);
1541                 }
1542             }
1543         }
1544 
1545         // And since all children are removed, remove the summary.
1546         removeCallback.accept(-1);
1547 
1548         // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated
1549         mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(),
1550                 summary.getKey());
1551     }
1552 
1553     /**
1554      * Updates the visibility of the bubbles based on current state.
1555      * Does not un-bubble, just hides or un-hides.
1556      * Updates stack description for TalkBack focus.
1557      * Updates bubbles' icon views clickable states
1558      */
updateStack()1559     public void updateStack() {
1560         if (mStackView == null) {
1561             return;
1562         }
1563 
1564         if (!mIsStatusBarShade) {
1565             // Bubbles don't appear over the locked shade.
1566             mStackView.setVisibility(INVISIBLE);
1567         } else if (hasBubbles()) {
1568             // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
1569             // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
1570             // out.
1571             mStackView.setVisibility(VISIBLE);
1572         }
1573 
1574         mStackView.updateContentDescription();
1575 
1576         mStackView.updateBubblesAcessibillityStates();
1577     }
1578 
1579     @VisibleForTesting
getStackView()1580     public BubbleStackView getStackView() {
1581         return mStackView;
1582     }
1583 
1584     /**
1585      * Check if notification panel is in an expanded state.
1586      * Makes a call to System UI process and delivers the result via {@code callback} on the
1587      * WM Shell main thread.
1588      *
1589      * @param callback callback that has the result of notification panel expanded state
1590      */
isNotificationPanelExpanded(Consumer<Boolean> callback)1591     public void isNotificationPanelExpanded(Consumer<Boolean> callback) {
1592         mSysuiProxy.isNotificationPanelExpand(expanded ->
1593                 mMainExecutor.execute(() -> callback.accept(expanded)));
1594     }
1595 
1596     /**
1597      * Description of current bubble state.
1598      */
dump(PrintWriter pw, String prefix)1599     private void dump(PrintWriter pw, String prefix) {
1600         pw.println("BubbleController state:");
1601         mBubbleData.dump(pw);
1602         pw.println();
1603         if (mStackView != null) {
1604             mStackView.dump(pw);
1605         }
1606         pw.println();
1607         mImpl.mCachedState.dump(pw);
1608     }
1609 
1610     /**
1611      * Whether an intent is properly configured to display in a
1612      * {@link com.android.wm.shell.TaskView}.
1613      *
1614      * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically
1615      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
1616      *
1617      * @param context the context to use.
1618      * @param entry   the entry to bubble.
1619      */
canLaunchInTaskView(Context context, BubbleEntry entry)1620     static boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
1621         PendingIntent intent = entry.getBubbleMetadata() != null
1622                 ? entry.getBubbleMetadata().getIntent()
1623                 : null;
1624         if (entry.getBubbleMetadata() != null
1625                 && entry.getBubbleMetadata().getShortcutId() != null) {
1626             return true;
1627         }
1628         if (intent == null) {
1629             Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey());
1630             return false;
1631         }
1632         PackageManager packageManager = getPackageManagerForUser(
1633                 context, entry.getStatusBarNotification().getUser().getIdentifier());
1634         return isResizableActivity(intent.getIntent(), packageManager, entry.getKey());
1635     }
1636 
isResizableActivity(Intent intent, PackageManager packageManager, String key)1637     static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) {
1638         if (intent == null) {
1639             Log.w(TAG, "Unable to send as bubble: " + key + " null intent");
1640             return false;
1641         }
1642         ActivityInfo info = intent.resolveActivityInfo(packageManager, 0);
1643         if (info == null) {
1644             Log.w(TAG, "Unable to send as bubble: " + key
1645                     + " couldn't find activity info for intent: " + intent);
1646             return false;
1647         }
1648         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
1649             Log.w(TAG, "Unable to send as bubble: " + key
1650                     + " activity is not resizable for intent: " + intent);
1651             return false;
1652         }
1653         return true;
1654     }
1655 
getPackageManagerForUser(Context context, int userId)1656     static PackageManager getPackageManagerForUser(Context context, int userId) {
1657         Context contextForUser = context;
1658         // UserHandle defines special userId as negative values, e.g. USER_ALL
1659         if (userId >= 0) {
1660             try {
1661                 // Create a context for the correct user so if a package isn't installed
1662                 // for user 0 we can still load information about the package.
1663                 contextForUser =
1664                         context.createPackageContextAsUser(context.getPackageName(),
1665                                 Context.CONTEXT_RESTRICTED,
1666                                 new UserHandle(userId));
1667             } catch (PackageManager.NameNotFoundException e) {
1668                 // Shouldn't fail to find the package name for system ui.
1669             }
1670         }
1671         return contextForUser.getPackageManager();
1672     }
1673 
1674     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
1675     //TODO(b/170442945): Better way to do this / insets listener?
1676     private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener {
1677         @Override
onImeVisibilityChanged(boolean imeVisible, int imeHeight)1678         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
1679             mBubblePositioner.setImeVisible(imeVisible, imeHeight);
1680             if (mStackView != null) {
1681                 mStackView.setImeVisible(imeVisible);
1682             }
1683         }
1684     }
1685 
1686     private class BubblesImpl implements Bubbles {
1687         // Up-to-date cached state of bubbles data for SysUI to query from the calling thread
1688         @VisibleForTesting
1689         public class CachedState {
1690             private boolean mIsStackExpanded;
1691             private String mSelectedBubbleKey;
1692             private HashSet<String> mSuppressedBubbleKeys = new HashSet<>();
1693             private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>();
1694             private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>();
1695 
1696             private ArrayList<Bubble> mTmpBubbles = new ArrayList<>();
1697 
1698             /**
1699              * Updates the cached state based on the last full BubbleData change.
1700              */
update(BubbleData.Update update)1701             synchronized void update(BubbleData.Update update) {
1702                 if (update.selectionChanged) {
1703                     mSelectedBubbleKey = update.selectedBubble != null
1704                             ? update.selectedBubble.getKey()
1705                             : null;
1706                 }
1707                 if (update.expandedChanged) {
1708                     mIsStackExpanded = update.expanded;
1709                 }
1710                 if (update.suppressedSummaryChanged) {
1711                     String summaryKey =
1712                             mBubbleData.getSummaryKey(update.suppressedSummaryGroup);
1713                     if (summaryKey != null) {
1714                         mSuppressedGroupToNotifKeys.put(update.suppressedSummaryGroup, summaryKey);
1715                     } else {
1716                         mSuppressedGroupToNotifKeys.remove(update.suppressedSummaryGroup);
1717                     }
1718                 }
1719 
1720                 mTmpBubbles.clear();
1721                 mTmpBubbles.addAll(update.bubbles);
1722                 mTmpBubbles.addAll(update.overflowBubbles);
1723 
1724                 mSuppressedBubbleKeys.clear();
1725                 mShortcutIdToBubble.clear();
1726                 for (Bubble b : mTmpBubbles) {
1727                     mShortcutIdToBubble.put(b.getShortcutId(), b);
1728                     updateBubbleSuppressedState(b);
1729                 }
1730             }
1731 
1732             /**
1733              * Updates a specific bubble suppressed state.  This is used mainly because notification
1734              * suppression changes don't go through the same BubbleData update mechanism.
1735              */
updateBubbleSuppressedState(Bubble b)1736             synchronized void updateBubbleSuppressedState(Bubble b) {
1737                 if (!b.showInShade()) {
1738                     mSuppressedBubbleKeys.add(b.getKey());
1739                 } else {
1740                     mSuppressedBubbleKeys.remove(b.getKey());
1741                 }
1742             }
1743 
isStackExpanded()1744             public synchronized boolean isStackExpanded() {
1745                 return mIsStackExpanded;
1746             }
1747 
isBubbleExpanded(String key)1748             public synchronized boolean isBubbleExpanded(String key) {
1749                 return mIsStackExpanded && key.equals(mSelectedBubbleKey);
1750             }
1751 
isBubbleNotificationSuppressedFromShade(String key, String groupKey)1752             public synchronized boolean isBubbleNotificationSuppressedFromShade(String key,
1753                     String groupKey) {
1754                 return mSuppressedBubbleKeys.contains(key)
1755                         || (mSuppressedGroupToNotifKeys.containsKey(groupKey)
1756                         && key.equals(mSuppressedGroupToNotifKeys.get(groupKey)));
1757             }
1758 
1759             @Nullable
getBubbleWithShortcutId(String id)1760             public synchronized Bubble getBubbleWithShortcutId(String id) {
1761                 return mShortcutIdToBubble.get(id);
1762             }
1763 
dump(PrintWriter pw)1764             synchronized void dump(PrintWriter pw) {
1765                 pw.println("BubbleImpl.CachedState state:");
1766 
1767                 pw.println("mIsStackExpanded: " + mIsStackExpanded);
1768                 pw.println("mSelectedBubbleKey: " + mSelectedBubbleKey);
1769 
1770                 pw.print("mSuppressedBubbleKeys: ");
1771                 pw.println(mSuppressedBubbleKeys.size());
1772                 for (String key : mSuppressedBubbleKeys) {
1773                     pw.println("   suppressing: " + key);
1774                 }
1775 
1776                 pw.print("mSuppressedGroupToNotifKeys: ");
1777                 pw.println(mSuppressedGroupToNotifKeys.size());
1778                 for (String key : mSuppressedGroupToNotifKeys.keySet()) {
1779                     pw.println("   suppressing: " + key);
1780                 }
1781             }
1782         }
1783 
1784         private CachedState mCachedState = new CachedState();
1785 
1786         @Override
isBubbleNotificationSuppressedFromShade(String key, String groupKey)1787         public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
1788             return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey);
1789         }
1790 
1791         @Override
isBubbleExpanded(String key)1792         public boolean isBubbleExpanded(String key) {
1793             return mCachedState.isBubbleExpanded(key);
1794         }
1795 
1796         @Override
1797         @Nullable
getBubbleWithShortcutId(String shortcutId)1798         public Bubble getBubbleWithShortcutId(String shortcutId) {
1799             return mCachedState.getBubbleWithShortcutId(shortcutId);
1800         }
1801 
1802         @Override
collapseStack()1803         public void collapseStack() {
1804             mMainExecutor.execute(() -> {
1805                 BubbleController.this.collapseStack();
1806             });
1807         }
1808 
1809         @Override
expandStackAndSelectBubble(BubbleEntry entry)1810         public void expandStackAndSelectBubble(BubbleEntry entry) {
1811             mMainExecutor.execute(() -> {
1812                 BubbleController.this.expandStackAndSelectBubble(entry);
1813             });
1814         }
1815 
1816         @Override
expandStackAndSelectBubble(Bubble bubble)1817         public void expandStackAndSelectBubble(Bubble bubble) {
1818             mMainExecutor.execute(() -> {
1819                 BubbleController.this.expandStackAndSelectBubble(bubble);
1820             });
1821         }
1822 
1823         @Override
showOrHideAppBubble(Intent intent)1824         public void showOrHideAppBubble(Intent intent) {
1825             mMainExecutor.execute(() -> {
1826                 BubbleController.this.showOrHideAppBubble(intent);
1827             });
1828         }
1829 
1830         @Override
handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback, Executor callbackExecutor)1831         public boolean handleDismissalInterception(BubbleEntry entry,
1832                 @Nullable List<BubbleEntry> children, IntConsumer removeCallback,
1833                 Executor callbackExecutor) {
1834             IntConsumer cb = removeCallback != null
1835                     ? (index) -> callbackExecutor.execute(() -> removeCallback.accept(index))
1836                     : null;
1837             return mMainExecutor.executeBlockingForResult(() -> {
1838                 return BubbleController.this.handleDismissalInterception(entry, children, cb);
1839             }, Boolean.class);
1840         }
1841 
1842         @Override
setSysuiProxy(SysuiProxy proxy)1843         public void setSysuiProxy(SysuiProxy proxy) {
1844             mMainExecutor.execute(() -> {
1845                 BubbleController.this.setSysuiProxy(proxy);
1846             });
1847         }
1848 
1849         @Override
setExpandListener(BubbleExpandListener listener)1850         public void setExpandListener(BubbleExpandListener listener) {
1851             mMainExecutor.execute(() -> {
1852                 BubbleController.this.setExpandListener(listener);
1853             });
1854         }
1855 
1856         @Override
onEntryAdded(BubbleEntry entry)1857         public void onEntryAdded(BubbleEntry entry) {
1858             mMainExecutor.execute(() -> {
1859                 BubbleController.this.onEntryAdded(entry);
1860             });
1861         }
1862 
1863         @Override
onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem)1864         public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) {
1865             mMainExecutor.execute(() -> {
1866                 BubbleController.this.onEntryUpdated(entry, shouldBubbleUp, fromSystem);
1867             });
1868         }
1869 
1870         @Override
onEntryRemoved(BubbleEntry entry)1871         public void onEntryRemoved(BubbleEntry entry) {
1872             mMainExecutor.execute(() -> {
1873                 BubbleController.this.onEntryRemoved(entry);
1874             });
1875         }
1876 
1877         @Override
onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)1878         public void onRankingUpdated(RankingMap rankingMap,
1879                 HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) {
1880             mMainExecutor.execute(() -> {
1881                 BubbleController.this.onRankingUpdated(rankingMap, entryDataByKey);
1882             });
1883         }
1884 
1885         @Override
onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)1886         public void onNotificationChannelModified(String pkg,
1887                 UserHandle user, NotificationChannel channel, int modificationType) {
1888             // Bubbles only cares about updates or deletions.
1889             if (modificationType == NOTIFICATION_CHANNEL_OR_GROUP_UPDATED
1890                     || modificationType == NOTIFICATION_CHANNEL_OR_GROUP_DELETED) {
1891                 mMainExecutor.execute(() -> {
1892                     BubbleController.this.onNotificationChannelModified(pkg, user, channel,
1893                             modificationType);
1894                 });
1895             }
1896         }
1897 
1898         @Override
onStatusBarVisibilityChanged(boolean visible)1899         public void onStatusBarVisibilityChanged(boolean visible) {
1900             mMainExecutor.execute(() -> {
1901                 BubbleController.this.onStatusBarVisibilityChanged(visible);
1902             });
1903         }
1904 
1905         @Override
onZenStateChanged()1906         public void onZenStateChanged() {
1907             mMainExecutor.execute(() -> {
1908                 BubbleController.this.onZenStateChanged();
1909             });
1910         }
1911 
1912         @Override
onStatusBarStateChanged(boolean isShade)1913         public void onStatusBarStateChanged(boolean isShade) {
1914             mMainExecutor.execute(() -> {
1915                 BubbleController.this.onStatusBarStateChanged(isShade);
1916             });
1917         }
1918 
1919         @Override
onUserChanged(int newUserId)1920         public void onUserChanged(int newUserId) {
1921             mMainExecutor.execute(() -> {
1922                 BubbleController.this.onUserChanged(newUserId);
1923             });
1924         }
1925 
1926         @Override
onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)1927         public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {
1928             mMainExecutor.execute(() -> {
1929                 BubbleController.this.onCurrentProfilesChanged(currentProfiles);
1930             });
1931         }
1932 
1933         @Override
onUserRemoved(int removedUserId)1934         public void onUserRemoved(int removedUserId) {
1935             mMainExecutor.execute(() -> {
1936                 BubbleController.this.onUserRemoved(removedUserId);
1937             });
1938         }
1939 
1940         @Override
onNotificationPanelExpandedChanged(boolean expanded)1941         public void onNotificationPanelExpandedChanged(boolean expanded) {
1942             mMainExecutor.execute(
1943                     () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
1944         }
1945     }
1946 
1947     /**
1948      * Bubble data that is stored per user.
1949      * Used to store and restore active bubbles during user switching.
1950      */
1951     private static class UserBubbleData {
1952         private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>();
1953 
1954         /**
1955          * Add bubble key and whether it should be shown in notification shade
1956          */
add(String key, boolean shownInShade)1957         void add(String key, boolean shownInShade) {
1958             mKeyToShownInShadeMap.put(key, shownInShade);
1959         }
1960 
1961         /**
1962          * Get all bubble keys stored for this user
1963          */
getKeys()1964         Set<String> getKeys() {
1965             return mKeyToShownInShadeMap.keySet();
1966         }
1967 
1968         /**
1969          * Check if this bubble with the given key should be shown in the notification shade
1970          */
isShownInShade(String key)1971         boolean isShownInShade(String key) {
1972             return mKeyToShownInShadeMap.get(key);
1973         }
1974     }
1975 }
1976