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.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED; 20 import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED; 21 import static android.service.notification.NotificationListenerService.REASON_CANCEL; 22 import static android.view.View.INVISIBLE; 23 import static android.view.View.VISIBLE; 24 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 25 26 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 27 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 28 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_BLOCKED; 29 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_GROUP_CANCELLED; 30 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_INVALID_INTENT; 31 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NOTIF_CANCEL; 32 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_BUBBLE_UP; 33 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE; 34 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; 35 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; 36 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; 37 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; 38 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES; 39 40 import android.annotation.BinderThread; 41 import android.annotation.NonNull; 42 import android.annotation.UserIdInt; 43 import android.app.ActivityManager; 44 import android.app.Notification; 45 import android.app.NotificationChannel; 46 import android.app.PendingIntent; 47 import android.content.BroadcastReceiver; 48 import android.content.Context; 49 import android.content.Intent; 50 import android.content.IntentFilter; 51 import android.content.pm.ActivityInfo; 52 import android.content.pm.LauncherApps; 53 import android.content.pm.PackageManager; 54 import android.content.pm.ShortcutInfo; 55 import android.content.pm.UserInfo; 56 import android.content.res.Configuration; 57 import android.graphics.PixelFormat; 58 import android.graphics.Point; 59 import android.graphics.Rect; 60 import android.graphics.drawable.Icon; 61 import android.os.Binder; 62 import android.os.Bundle; 63 import android.os.Handler; 64 import android.os.RemoteException; 65 import android.os.ServiceManager; 66 import android.os.UserHandle; 67 import android.os.UserManager; 68 import android.service.notification.NotificationListenerService; 69 import android.service.notification.NotificationListenerService.RankingMap; 70 import android.util.Log; 71 import android.util.Pair; 72 import android.util.SparseArray; 73 import android.view.IWindowManager; 74 import android.view.SurfaceControl; 75 import android.view.View; 76 import android.view.ViewGroup; 77 import android.view.ViewRootImpl; 78 import android.view.WindowInsets; 79 import android.view.WindowManager; 80 import android.window.ScreenCapture; 81 import android.window.ScreenCapture.SynchronousScreenCaptureListener; 82 83 import androidx.annotation.MainThread; 84 import androidx.annotation.Nullable; 85 86 import com.android.internal.annotations.VisibleForTesting; 87 import com.android.internal.protolog.common.ProtoLog; 88 import com.android.internal.statusbar.IStatusBarService; 89 import com.android.internal.util.CollectionUtils; 90 import com.android.launcher3.icons.BubbleIconFactory; 91 import com.android.wm.shell.Flags; 92 import com.android.wm.shell.R; 93 import com.android.wm.shell.ShellTaskOrganizer; 94 import com.android.wm.shell.WindowManagerShellWrapper; 95 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; 96 import com.android.wm.shell.bubbles.properties.BubbleProperties; 97 import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper; 98 import com.android.wm.shell.common.DisplayController; 99 import com.android.wm.shell.common.ExternalInterfaceBinder; 100 import com.android.wm.shell.common.FloatingContentCoordinator; 101 import com.android.wm.shell.common.RemoteCallable; 102 import com.android.wm.shell.common.ShellExecutor; 103 import com.android.wm.shell.common.SingleInstanceRemoteListener; 104 import com.android.wm.shell.common.SyncTransactionQueue; 105 import com.android.wm.shell.common.TaskStackListenerCallback; 106 import com.android.wm.shell.common.TaskStackListenerImpl; 107 import com.android.wm.shell.common.bubbles.BubbleBarLocation; 108 import com.android.wm.shell.common.bubbles.BubbleBarUpdate; 109 import com.android.wm.shell.draganddrop.DragAndDropController; 110 import com.android.wm.shell.onehanded.OneHandedController; 111 import com.android.wm.shell.onehanded.OneHandedTransitionCallback; 112 import com.android.wm.shell.pip.PinnedStackListenerForwarder; 113 import com.android.wm.shell.shared.annotations.ShellBackgroundThread; 114 import com.android.wm.shell.shared.annotations.ShellMainThread; 115 import com.android.wm.shell.sysui.ConfigurationChangeListener; 116 import com.android.wm.shell.sysui.ShellCommandHandler; 117 import com.android.wm.shell.sysui.ShellController; 118 import com.android.wm.shell.sysui.ShellInit; 119 import com.android.wm.shell.taskview.TaskView; 120 import com.android.wm.shell.taskview.TaskViewTaskController; 121 import com.android.wm.shell.taskview.TaskViewTransitions; 122 import com.android.wm.shell.transition.Transitions; 123 124 import java.io.PrintWriter; 125 import java.util.ArrayList; 126 import java.util.HashMap; 127 import java.util.HashSet; 128 import java.util.List; 129 import java.util.Locale; 130 import java.util.Map; 131 import java.util.Objects; 132 import java.util.Optional; 133 import java.util.Set; 134 import java.util.concurrent.Executor; 135 import java.util.function.Consumer; 136 import java.util.function.IntConsumer; 137 138 /** 139 * Bubbles are a special type of content that can "float" on top of other apps or System UI. 140 * Bubbles can be expanded to show more content. 141 * 142 * The controller manages addition, removal, and visible state of bubbles on screen. 143 */ 144 public class BubbleController implements ConfigurationChangeListener, 145 RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider { 146 147 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; 148 149 // Should match with PhoneWindowManager 150 private static final String SYSTEM_DIALOG_REASON_KEY = "reason"; 151 private static final String SYSTEM_DIALOG_REASON_GESTURE_NAV = "gestureNav"; 152 private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; 153 private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey"; 154 155 /** 156 * Common interface to send updates to bubble views. 157 */ 158 public interface BubbleViewCallback { 159 /** Called when the provided bubble should be removed. */ removeBubble(Bubble removedBubble)160 void removeBubble(Bubble removedBubble); 161 /** Called when the provided bubble should be added. */ addBubble(Bubble addedBubble)162 void addBubble(Bubble addedBubble); 163 /** Called when the provided bubble should be updated. */ updateBubble(Bubble updatedBubble)164 void updateBubble(Bubble updatedBubble); 165 /** Called when the provided bubble should be selected. */ selectionChanged(BubbleViewProvider selectedBubble)166 void selectionChanged(BubbleViewProvider selectedBubble); 167 /** Called when the provided bubble's suppression state has changed. */ suppressionChanged(Bubble bubble, boolean isSuppressed)168 void suppressionChanged(Bubble bubble, boolean isSuppressed); 169 /** Called when the expansion state of bubbles has changed. */ expansionChanged(boolean isExpanded)170 void expansionChanged(boolean isExpanded); 171 /** 172 * Called when the order of the bubble list has changed. Depending on the expanded state 173 * the pointer might need to be updated. 174 */ bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer)175 void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer); 176 /** Called when the bubble overflow empty state changes, used to show/hide the overflow. */ bubbleOverflowChanged(boolean hasBubbles)177 void bubbleOverflowChanged(boolean hasBubbles); 178 } 179 180 private final Context mContext; 181 private final BubblesImpl mImpl = new BubblesImpl(); 182 private Bubbles.BubbleExpandListener mExpandListener; 183 @Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; 184 private final FloatingContentCoordinator mFloatingContentCoordinator; 185 private final BubbleDataRepository mDataRepository; 186 private final WindowManagerShellWrapper mWindowManagerShellWrapper; 187 private final UserManager mUserManager; 188 private final LauncherApps mLauncherApps; 189 private final IStatusBarService mBarService; 190 private final WindowManager mWindowManager; 191 private final TaskStackListenerImpl mTaskStackListener; 192 private final ShellTaskOrganizer mTaskOrganizer; 193 private final DisplayController mDisplayController; 194 private final TaskViewTransitions mTaskViewTransitions; 195 private final Transitions mTransitions; 196 private final SyncTransactionQueue mSyncQueue; 197 private final ShellController mShellController; 198 private final ShellCommandHandler mShellCommandHandler; 199 private final IWindowManager mWmService; 200 private final BubbleProperties mBubbleProperties; 201 private final BubbleTaskViewFactory mBubbleTaskViewFactory; 202 private final BubbleExpandedViewManager mExpandedViewManager; 203 204 // Used to post to main UI thread 205 private final ShellExecutor mMainExecutor; 206 private final Handler mMainHandler; 207 private final ShellExecutor mBackgroundExecutor; 208 209 private final BubbleLogger mLogger; 210 private final BubbleData mBubbleData; 211 @Nullable private BubbleStackView mStackView; 212 @Nullable private BubbleBarLayerView mLayerView; 213 private BubbleIconFactory mBubbleIconFactory; 214 private final BubblePositioner mBubblePositioner; 215 private Bubbles.SysuiProxy mSysuiProxy; 216 217 // Tracks the id of the current (foreground) user. 218 private int mCurrentUserId; 219 // Current profiles of the user (e.g. user with a workprofile) 220 private SparseArray<UserInfo> mCurrentProfiles; 221 // Saves data about active bubbles when users are switched. 222 private final SparseArray<UserBubbleData> mSavedUserBubbleData; 223 224 // Used when ranking updates occur and we check if things should bubble / unbubble 225 private NotificationListenerService.Ranking mTmpRanking; 226 227 // Callback that updates BubbleOverflowActivity on data change. 228 @Nullable private BubbleData.Listener mOverflowListener = null; 229 230 // Typically only load once & after user switches 231 private boolean mOverflowDataLoadNeeded = true; 232 233 /** 234 * When the shade status changes to SHADE (from anything but SHADE, like LOCKED) we'll select 235 * this bubble and expand the stack. 236 */ 237 @Nullable private BubbleEntry mNotifEntryToExpandOnShadeUnlock; 238 239 /** LayoutParams used to add the BubbleStackView to the window manager. */ 240 private WindowManager.LayoutParams mWmLayoutParams; 241 /** Whether or not the BubbleStackView has been added to the WindowManager. */ 242 private boolean mAddedToWindowManager = false; 243 244 /** 245 * Saved screen density, used to detect display size changes in {@link #onConfigurationChanged}. 246 */ 247 private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED; 248 249 /** 250 * Saved screen bounds, used to detect screen size changes in {@link #onConfigurationChanged}. 251 */ 252 private final Rect mScreenBounds = new Rect(); 253 254 /** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */ 255 private float mFontScale = 0; 256 257 /** Saved locale, used to detect local changes in {@link #onConfigurationChanged}. */ 258 private Locale mLocale = null; 259 260 /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */ 261 private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; 262 263 /** Saved insets, used to detect WindowInset changes. */ 264 private WindowInsets mWindowInsets; 265 266 private boolean mInflateSynchronously; 267 268 /** True when user is in status bar unlock shade. */ 269 private boolean mIsStatusBarShade = true; 270 271 /** One handed mode controller to register transition listener. */ 272 private final Optional<OneHandedController> mOneHandedOptional; 273 /** Drag and drop controller to register listener for onDragStarted. */ 274 private final DragAndDropController mDragAndDropController; 275 /** Used to send bubble events to launcher. */ 276 private Bubbles.BubbleStateListener mBubbleStateListener; 277 278 /** Used to send updates to the views from {@link #mBubbleDataListener}. */ 279 private BubbleViewCallback mBubbleViewCallback; 280 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, Transitions transitions, SyncTransactionQueue syncQueue, IWindowManager wmService, BubbleProperties bubbleProperties)281 public BubbleController(Context context, 282 ShellInit shellInit, 283 ShellCommandHandler shellCommandHandler, 284 ShellController shellController, 285 BubbleData data, 286 @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, 287 FloatingContentCoordinator floatingContentCoordinator, 288 BubbleDataRepository dataRepository, 289 @Nullable IStatusBarService statusBarService, 290 WindowManager windowManager, 291 WindowManagerShellWrapper windowManagerShellWrapper, 292 UserManager userManager, 293 LauncherApps launcherApps, 294 BubbleLogger bubbleLogger, 295 TaskStackListenerImpl taskStackListener, 296 ShellTaskOrganizer organizer, 297 BubblePositioner positioner, 298 DisplayController displayController, 299 Optional<OneHandedController> oneHandedOptional, 300 DragAndDropController dragAndDropController, 301 @ShellMainThread ShellExecutor mainExecutor, 302 @ShellMainThread Handler mainHandler, 303 @ShellBackgroundThread ShellExecutor bgExecutor, 304 TaskViewTransitions taskViewTransitions, 305 Transitions transitions, 306 SyncTransactionQueue syncQueue, 307 IWindowManager wmService, 308 BubbleProperties bubbleProperties) { 309 mContext = context; 310 mShellCommandHandler = shellCommandHandler; 311 mShellController = shellController; 312 mLauncherApps = launcherApps; 313 mBarService = statusBarService == null 314 ? IStatusBarService.Stub.asInterface( 315 ServiceManager.getService(Context.STATUS_BAR_SERVICE)) 316 : statusBarService; 317 mWindowManager = windowManager; 318 mWindowManagerShellWrapper = windowManagerShellWrapper; 319 mUserManager = userManager; 320 mFloatingContentCoordinator = floatingContentCoordinator; 321 mDataRepository = dataRepository; 322 mLogger = bubbleLogger; 323 mMainExecutor = mainExecutor; 324 mMainHandler = mainHandler; 325 mBackgroundExecutor = bgExecutor; 326 mTaskStackListener = taskStackListener; 327 mTaskOrganizer = organizer; 328 mSurfaceSynchronizer = synchronizer; 329 mCurrentUserId = ActivityManager.getCurrentUser(); 330 mBubblePositioner = positioner; 331 mBubbleData = data; 332 mSavedUserBubbleData = new SparseArray<>(); 333 mBubbleIconFactory = new BubbleIconFactory(context, 334 context.getResources().getDimensionPixelSize(R.dimen.bubble_size), 335 context.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size), 336 context.getResources().getColor( 337 com.android.launcher3.icons.R.color.important_conversation), 338 context.getResources().getDimensionPixelSize( 339 com.android.internal.R.dimen.importance_ring_stroke_width)); 340 mDisplayController = displayController; 341 mTaskViewTransitions = taskViewTransitions; 342 mTransitions = transitions; 343 mOneHandedOptional = oneHandedOptional; 344 mDragAndDropController = dragAndDropController; 345 mSyncQueue = syncQueue; 346 mWmService = wmService; 347 mBubbleProperties = bubbleProperties; 348 shellInit.addInitCallback(this::onInit, this); 349 mBubbleTaskViewFactory = new BubbleTaskViewFactory() { 350 @Override 351 public BubbleTaskView create() { 352 TaskViewTaskController taskViewTaskController = new TaskViewTaskController( 353 context, organizer, taskViewTransitions, syncQueue); 354 TaskView taskView = new TaskView(context, taskViewTaskController); 355 return new BubbleTaskView(taskView, mainExecutor); 356 } 357 }; 358 mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this); 359 } 360 registerOneHandedState(OneHandedController oneHanded)361 private void registerOneHandedState(OneHandedController oneHanded) { 362 oneHanded.registerTransitionCallback( 363 new OneHandedTransitionCallback() { 364 @Override 365 public void onStartFinished(Rect bounds) { 366 mMainExecutor.execute(() -> { 367 if (mStackView != null) { 368 mStackView.onVerticalOffsetChanged(bounds.top); 369 } 370 }); 371 } 372 373 @Override 374 public void onStopFinished(Rect bounds) { 375 mMainExecutor.execute(() -> { 376 if (mStackView != null) { 377 mStackView.onVerticalOffsetChanged(bounds.top); 378 } 379 }); 380 } 381 }); 382 } 383 onInit()384 protected void onInit() { 385 mBubbleViewCallback = isShowingAsBubbleBar() 386 ? mBubbleBarViewCallback 387 : mBubbleStackViewCallback; 388 mBubbleData.setListener(mBubbleDataListener); 389 mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); 390 mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); 391 392 mBubbleData.setPendingIntentCancelledListener(bubble -> { 393 if (bubble.getBubbleIntent() == null) { 394 return; 395 } 396 if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 397 bubble.setPendingIntentCanceled(); 398 return; 399 } 400 mMainExecutor.execute(() -> removeBubble(bubble.getKey(), DISMISS_INVALID_INTENT)); 401 }); 402 403 try { 404 mWindowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener()); 405 } catch (RemoteException e) { 406 e.printStackTrace(); 407 } 408 409 mBubbleData.setCurrentUserId(mCurrentUserId); 410 411 mTaskOrganizer.addLocusIdListener((taskId, locus, visible) -> 412 mBubbleData.onLocusVisibilityChanged(taskId, locus, visible)); 413 414 mLauncherApps.registerCallback(new LauncherApps.Callback() { 415 @Override 416 public void onPackageAdded(String s, UserHandle userHandle) {} 417 418 @Override 419 public void onPackageChanged(String s, UserHandle userHandle) {} 420 421 @Override 422 public void onPackageRemoved(String s, UserHandle userHandle) { 423 // Remove bubbles with this package name, since it has been uninstalled and attempts 424 // to open a bubble from an uninstalled app can cause issues. 425 mBubbleData.removeBubblesWithPackageName(s, DISMISS_PACKAGE_REMOVED); 426 } 427 428 @Override 429 public void onPackagesAvailable(String[] strings, UserHandle userHandle, boolean b) {} 430 431 @Override 432 public void onPackagesUnavailable(String[] packages, UserHandle userHandle, 433 boolean b) { 434 for (String packageName : packages) { 435 // Remove bubbles from unavailable apps. This can occur when the app is on 436 // external storage that has been removed. 437 mBubbleData.removeBubblesWithPackageName(packageName, DISMISS_PACKAGE_REMOVED); 438 } 439 } 440 441 @Override 442 public void onShortcutsChanged(String packageName, List<ShortcutInfo> validShortcuts, 443 UserHandle user) { 444 super.onShortcutsChanged(packageName, validShortcuts, user); 445 446 // Remove bubbles whose shortcuts aren't in the latest list of valid shortcuts. 447 mBubbleData.removeBubblesWithInvalidShortcuts( 448 packageName, validShortcuts, DISMISS_SHORTCUT_REMOVED); 449 } 450 }, mMainHandler); 451 452 mTransitions.registerObserver(new BubblesTransitionObserver(this, mBubbleData)); 453 454 mTaskStackListener.addListener(new TaskStackListenerCallback() { 455 @Override 456 public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, 457 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { 458 for (Bubble b : mBubbleData.getBubbles()) { 459 if (task.taskId == b.getTaskId()) { 460 ProtoLog.d(WM_SHELL_BUBBLES, 461 "onActivityRestartAttempt - taskId=%d selecting matching bubble=%s", 462 task.taskId, b.getKey()); 463 mBubbleData.setSelectedBubbleAndExpandStack(b); 464 return; 465 } 466 } 467 for (Bubble b : mBubbleData.getOverflowBubbles()) { 468 if (task.taskId == b.getTaskId()) { 469 ProtoLog.d(WM_SHELL_BUBBLES, "onActivityRestartAttempt - taskId=%d " 470 + "selecting matching overflow bubble=%s", 471 task.taskId, b.getKey()); 472 promoteBubbleFromOverflow(b); 473 mBubbleData.setExpanded(true); 474 return; 475 } 476 } 477 } 478 }); 479 480 mDisplayController.addDisplayChangingController( 481 (displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { 482 Rect newScreenBounds = new Rect(); 483 if (newDisplayAreaInfo != null) { 484 newScreenBounds = 485 newDisplayAreaInfo.configuration.windowConfiguration.getBounds(); 486 } 487 // This is triggered right before the rotation or new screen size is applied 488 if (fromRotation != toRotation || !newScreenBounds.equals(mScreenBounds)) { 489 if (mStackView != null) { 490 // Layout listener set on stackView will update the positioner 491 // once the rotation or screen change is applied 492 mStackView.onOrientationChanged(); 493 } 494 } 495 }); 496 497 mOneHandedOptional.ifPresent(this::registerOneHandedState); 498 mDragAndDropController.addListener(new DragAndDropController.DragAndDropListener() { 499 @Override 500 public void onDragStarted() { 501 collapseStack(); 502 } 503 }); 504 505 // Clear out any persisted bubbles on disk that no longer have a valid user. 506 List<UserInfo> users = mUserManager.getAliveUsers(); 507 mDataRepository.sanitizeBubbles(users); 508 509 // Init profiles 510 SparseArray<UserInfo> userProfiles = new SparseArray<>(); 511 for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) { 512 userProfiles.put(user.id, user); 513 } 514 mCurrentProfiles = userProfiles; 515 516 if (Flags.enableRetrievableBubbles()) { 517 registerShortcutBroadcastReceiver(); 518 } 519 520 mShellController.addConfigurationChangeListener(this); 521 mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES, 522 this::createExternalInterface, this); 523 mShellCommandHandler.addDumpCallback(this::dump, this); 524 } 525 createExternalInterface()526 private ExternalInterfaceBinder createExternalInterface() { 527 return new IBubblesImpl(this); 528 } 529 530 @VisibleForTesting asBubbles()531 public Bubbles asBubbles() { 532 return mImpl; 533 } 534 535 @VisibleForTesting getImplCachedState()536 public BubblesImpl.CachedState getImplCachedState() { 537 return mImpl.mCachedState; 538 } 539 getMainExecutor()540 public ShellExecutor getMainExecutor() { 541 return mMainExecutor; 542 } 543 544 @Override getContext()545 public Context getContext() { 546 return mContext; 547 } 548 549 @Override getRemoteCallExecutor()550 public ShellExecutor getRemoteCallExecutor() { 551 return mMainExecutor; 552 } 553 554 /** 555 * Sets a listener to be notified of bubble updates. This is used by launcher so that 556 * it may render bubbles in itself. Only one listener is supported. 557 * 558 * <p>If bubble bar is supported, bubble views will be updated to switch to bar mode. 559 */ registerBubbleStateListener(Bubbles.BubbleStateListener listener)560 public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) { 561 mBubbleProperties.refresh(); 562 if (canShowAsBubbleBar() && listener != null) { 563 // Only set the listener if we can show the bubble bar. 564 mBubbleStateListener = listener; 565 setUpBubbleViewsForMode(); 566 sendInitialListenerUpdate(); 567 } else { 568 mBubbleStateListener = null; 569 } 570 } 571 572 /** 573 * Unregisters the {@link Bubbles.BubbleStateListener}. 574 * 575 * <p>If there's an existing listener, then we're switching back to stack mode and bubble views 576 * will be updated accordingly. 577 */ unregisterBubbleStateListener()578 public void unregisterBubbleStateListener() { 579 mBubbleProperties.refresh(); 580 if (mBubbleStateListener != null) { 581 mBubbleStateListener = null; 582 setUpBubbleViewsForMode(); 583 } 584 } 585 586 /** 587 * If a {@link Bubbles.BubbleStateListener} is present, this will send the current bubble 588 * state to it. 589 */ sendInitialListenerUpdate()590 private void sendInitialListenerUpdate() { 591 if (mBubbleStateListener != null) { 592 BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar(); 593 mBubbleStateListener.onBubbleStateChange(update); 594 } 595 } 596 597 /** 598 * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. 599 */ hideCurrentInputMethod()600 void hideCurrentInputMethod() { 601 mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */); 602 int displayId = mWindowManager.getDefaultDisplay().getDisplayId(); 603 try { 604 mBarService.hideCurrentInputMethodForBubbles(displayId); 605 } catch (RemoteException e) { 606 Log.e(TAG, "Failed to hide IME", e); 607 } 608 } 609 610 /** 611 * Called when the status bar has become visible or invisible (either permanently or 612 * temporarily). 613 */ onStatusBarVisibilityChanged(boolean visible)614 private void onStatusBarVisibilityChanged(boolean visible) { 615 if (mStackView != null) { 616 // Hide the stack temporarily if the status bar has been made invisible, and the stack 617 // is collapsed. An expanded stack should remain visible until collapsed. 618 mStackView.setTemporarilyInvisible(!visible && !isStackExpanded()); 619 ProtoLog.d(WM_SHELL_BUBBLES, "onStatusBarVisibilityChanged=%b stackExpanded=%b", 620 visible, isStackExpanded()); 621 } 622 } 623 onZenStateChanged()624 private void onZenStateChanged() { 625 if (hasBubbles()) { 626 ProtoLog.d(WM_SHELL_BUBBLES, "onZenStateChanged"); 627 } 628 for (Bubble b : mBubbleData.getBubbles()) { 629 b.setShowDot(b.showInShade()); 630 } 631 } 632 633 @VisibleForTesting onStatusBarStateChanged(boolean isShade)634 public void onStatusBarStateChanged(boolean isShade) { 635 boolean didChange = mIsStatusBarShade != isShade; 636 ProtoLog.d(WM_SHELL_BUBBLES, "onStatusBarStateChanged " 637 + "isShade=%b didChange=%b mNotifEntryToExpandOnShadeUnlock=%s", 638 isShade, didChange, (mNotifEntryToExpandOnShadeUnlock != null 639 ? mNotifEntryToExpandOnShadeUnlock.getKey() : "null")); 640 mIsStatusBarShade = isShade; 641 if (!mIsStatusBarShade && didChange) { 642 // Only collapse stack on change 643 collapseStack(); 644 } 645 646 if (mNotifEntryToExpandOnShadeUnlock != null) { 647 expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock); 648 } 649 650 updateBubbleViews(); 651 } 652 653 @VisibleForTesting onBubbleMetadataFlagChanged(Bubble bubble)654 public void onBubbleMetadataFlagChanged(Bubble bubble) { 655 ProtoLog.d(WM_SHELL_BUBBLES, "onBubbleMetadataFlagChanged=%s flags=%d", 656 bubble.getKey(), bubble.getFlags()); 657 // Make sure NoMan knows suppression state so that anyone querying it can tell. 658 try { 659 mBarService.onBubbleMetadataFlagChanged(bubble.getKey(), bubble.getFlags()); 660 } catch (RemoteException e) { 661 // Bad things have happened 662 } 663 mImpl.mCachedState.updateBubbleSuppressedState(bubble); 664 } 665 666 /** Called when the current user changes. */ 667 @VisibleForTesting onUserChanged(int newUserId)668 public void onUserChanged(int newUserId) { 669 ProtoLog.d(WM_SHELL_BUBBLES, "onUserChanged currentUser=%d newUser=%d", 670 mCurrentUserId, newUserId); 671 saveBubbles(mCurrentUserId); 672 mCurrentUserId = newUserId; 673 674 mBubbleData.dismissAll(DISMISS_USER_CHANGED); 675 mBubbleData.clearOverflow(); 676 mOverflowDataLoadNeeded = true; 677 678 restoreBubbles(newUserId); 679 mBubbleData.setCurrentUserId(newUserId); 680 } 681 682 /** Called when the profiles for the current user change. **/ onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)683 public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) { 684 mCurrentProfiles = currentProfiles; 685 } 686 687 /** Called when a user is removed from the device, including work profiles. */ onUserRemoved(int removedUserId)688 public void onUserRemoved(int removedUserId) { 689 UserInfo parent = mUserManager.getProfileParent(removedUserId); 690 int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1; 691 mBubbleData.removeBubblesForUser(removedUserId); 692 // Typically calls from BubbleData would remove bubbles from the DataRepository as well, 693 // however, this gets complicated when users are removed (mCurrentUserId won't necessarily 694 // be correct for this) so we update the repo directly. 695 mDataRepository.removeBubblesForUser(removedUserId, parentUserId); 696 } 697 698 /** Called when sensitive notification state has changed */ onSensitiveNotificationProtectionStateChanged( boolean sensitiveNotificationProtectionActive)699 public void onSensitiveNotificationProtectionStateChanged( 700 boolean sensitiveNotificationProtectionActive) { 701 if (mStackView != null) { 702 mStackView.onSensitiveNotificationProtectionStateChanged( 703 sensitiveNotificationProtectionActive); 704 ProtoLog.d(WM_SHELL_BUBBLES, "onSensitiveNotificationProtectionStateChanged=%b", 705 sensitiveNotificationProtectionActive); 706 } 707 } 708 709 /** Whether bubbles are showing in the bubble bar. */ isShowingAsBubbleBar()710 public boolean isShowingAsBubbleBar() { 711 return canShowAsBubbleBar() && mBubbleStateListener != null; 712 } 713 714 /** Whether the current configuration supports showing as bubble bar. */ canShowAsBubbleBar()715 private boolean canShowAsBubbleBar() { 716 return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen(); 717 } 718 719 /** 720 * Returns current {@link BubbleBarLocation} if bubble bar is being used. 721 * Otherwise returns <code>null</code> 722 */ 723 @Nullable getBubbleBarLocation()724 public BubbleBarLocation getBubbleBarLocation() { 725 if (canShowAsBubbleBar()) { 726 return mBubblePositioner.getBubbleBarLocation(); 727 } 728 return null; 729 } 730 731 /** 732 * Update bubble bar location and trigger and update to listeners 733 */ setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)734 public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 735 if (canShowAsBubbleBar()) { 736 mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); 737 BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); 738 bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; 739 mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); 740 } 741 } 742 743 /** 744 * Animate bubble bar to the given location. The location change is transient. It does not 745 * update the state of the bubble bar. 746 * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. 747 */ animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation)748 public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 749 if (canShowAsBubbleBar()) { 750 mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation); 751 } 752 } 753 754 /** Whether this userId belongs to the current user. */ isCurrentProfile(int userId)755 private boolean isCurrentProfile(int userId) { 756 return userId == UserHandle.USER_ALL 757 || (mCurrentProfiles != null && mCurrentProfiles.get(userId) != null); 758 } 759 760 /** 761 * Sets whether to perform inflation on the same thread as the caller. This method should only 762 * be used in tests, not in production. 763 */ 764 @VisibleForTesting setInflateSynchronously(boolean inflateSynchronously)765 public void setInflateSynchronously(boolean inflateSynchronously) { 766 mInflateSynchronously = inflateSynchronously; 767 } 768 769 /** Set a listener to be notified of when overflow view update. */ setOverflowListener(BubbleData.Listener listener)770 public void setOverflowListener(BubbleData.Listener listener) { 771 mOverflowListener = listener; 772 } 773 774 /** 775 * @return Bubbles for updating overflow. 776 */ getOverflowBubbles()777 List<Bubble> getOverflowBubbles() { 778 return mBubbleData.getOverflowBubbles(); 779 } 780 781 /** The task listener for events in bubble tasks. */ getTaskOrganizer()782 public ShellTaskOrganizer getTaskOrganizer() { 783 return mTaskOrganizer; 784 } 785 getSyncTransactionQueue()786 SyncTransactionQueue getSyncTransactionQueue() { 787 return mSyncQueue; 788 } 789 getTaskViewTransitions()790 TaskViewTransitions getTaskViewTransitions() { 791 return mTaskViewTransitions; 792 } 793 794 /** Contains information to help position things on the screen. */ 795 @VisibleForTesting getPositioner()796 public BubblePositioner getPositioner() { 797 return mBubblePositioner; 798 } 799 getIconFactory()800 BubbleIconFactory getIconFactory() { 801 return mBubbleIconFactory; 802 } 803 804 @Override getSysuiProxy()805 public Bubbles.SysuiProxy getSysuiProxy() { 806 return mSysuiProxy; 807 } 808 809 /** 810 * The view and window for bubbles is lazily created by this method the first time a Bubble 811 * is added. Depending on the device state, this method will: 812 * - initialize a {@link BubbleStackView} and add it to window manager OR 813 * - initialize a {@link com.android.wm.shell.bubbles.bar.BubbleBarLayerView} and adds 814 * it to window manager. 815 */ ensureBubbleViewsAndWindowCreated()816 private void ensureBubbleViewsAndWindowCreated() { 817 mBubblePositioner.setShowingInBubbleBar(isShowingAsBubbleBar()); 818 if (isShowingAsBubbleBar()) { 819 // When we're showing in launcher / bubble bar is enabled, we don't have bubble stack 820 // view, instead we just show the expanded bubble view as necessary. We still need a 821 // window to show this in, but we use a separate code path. 822 // TODO(b/273312602): consider foldables where we do need a stack view when folded 823 if (mLayerView == null) { 824 mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData); 825 mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); 826 } 827 } else { 828 if (mStackView == null) { 829 BubbleStackViewManager bubbleStackViewManager = 830 BubbleStackViewManager.fromBubbleController(this); 831 mStackView = new BubbleStackView( 832 mContext, bubbleStackViewManager, mBubblePositioner, mBubbleData, 833 mSurfaceSynchronizer, mFloatingContentCoordinator, this, mMainExecutor); 834 mStackView.onOrientationChanged(); 835 if (mExpandListener != null) { 836 mStackView.setExpandListener(mExpandListener); 837 } 838 mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); 839 } 840 } 841 addToWindowManagerMaybe(); 842 } 843 844 /** Adds the appropriate view to WindowManager if it's not already there. */ addToWindowManagerMaybe()845 private void addToWindowManagerMaybe() { 846 // If already added, don't add it. 847 if (mAddedToWindowManager) { 848 return; 849 } 850 // If the appropriate view is null, don't add it. 851 if (isShowingAsBubbleBar() && mLayerView == null) { 852 return; 853 } else if (!isShowingAsBubbleBar() && mStackView == null) { 854 return; 855 } 856 857 mWmLayoutParams = new WindowManager.LayoutParams( 858 // Fill the screen so we can use translation animations to position the bubble 859 // views. We'll use touchable regions to ignore touches that are not on the bubbles 860 // themselves. 861 ViewGroup.LayoutParams.MATCH_PARENT, 862 ViewGroup.LayoutParams.MATCH_PARENT, 863 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, 864 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 865 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 866 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, 867 PixelFormat.TRANSLUCENT); 868 869 mWmLayoutParams.setTrustedOverlay(); 870 mWmLayoutParams.setFitInsetsTypes(0); 871 mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 872 mWmLayoutParams.token = new Binder(); 873 mWmLayoutParams.setTitle("Bubbles!"); 874 mWmLayoutParams.packageName = mContext.getPackageName(); 875 mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 876 mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 877 878 try { 879 mAddedToWindowManager = true; 880 registerBroadcastReceiver(); 881 if (isShowingAsBubbleBar()) { 882 mBubbleData.getOverflow().initializeForBubbleBar( 883 mExpandedViewManager, mBubblePositioner); 884 } else { 885 mBubbleData.getOverflow().initialize( 886 mExpandedViewManager, mStackView, mBubblePositioner); 887 } 888 // (TODO: b/273314541) some duplication in the inset listener 889 if (isShowingAsBubbleBar()) { 890 mWindowManager.addView(mLayerView, mWmLayoutParams); 891 mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> { 892 if (!windowInsets.equals(mWindowInsets) && mLayerView != null) { 893 mWindowInsets = windowInsets; 894 mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager)); 895 mLayerView.onDisplaySizeChanged(); 896 } 897 return windowInsets; 898 }); 899 } else { 900 mWindowManager.addView(mStackView, mWmLayoutParams); 901 mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> { 902 if (!windowInsets.equals(mWindowInsets) && mStackView != null) { 903 mWindowInsets = windowInsets; 904 mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager)); 905 mStackView.onDisplaySizeChanged(); 906 } 907 return windowInsets; 908 }); 909 } 910 } catch (IllegalStateException e) { 911 // This means the view has already been added. This shouldn't happen... 912 e.printStackTrace(); 913 } 914 } 915 916 /** 917 * In some situations bubble's should be able to receive key events for back: 918 * - when the bubble overflow is showing 919 * - when the user education for the stack is showing. 920 * 921 * @param interceptBack whether back should be intercepted or not. 922 */ updateWindowFlagsForBackpress(boolean interceptBack)923 void updateWindowFlagsForBackpress(boolean interceptBack) { 924 if (mAddedToWindowManager) { 925 ProtoLog.d(WM_SHELL_BUBBLES, "updateFlagsForBackPress interceptBack=%b", interceptBack); 926 mWmLayoutParams.flags = interceptBack 927 ? 0 928 : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 929 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 930 mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; 931 if (mStackView != null) { 932 mWindowManager.updateViewLayout(mStackView, mWmLayoutParams); 933 } else if (mLayerView != null) { 934 mWindowManager.updateViewLayout(mLayerView, mWmLayoutParams); 935 } 936 } 937 } 938 939 /** Removes any bubble views from the WindowManager that exist. */ removeFromWindowManagerMaybe()940 private void removeFromWindowManagerMaybe() { 941 if (!mAddedToWindowManager) { 942 return; 943 } 944 945 mAddedToWindowManager = false; 946 // Put on background for this binder call, was causing jank 947 mBackgroundExecutor.execute(() -> { 948 try { 949 mContext.unregisterReceiver(mBroadcastReceiver); 950 } catch (IllegalArgumentException e) { 951 // Not sure if this happens in production, but was happening in tests 952 // (b/253647225) 953 e.printStackTrace(); 954 } 955 }); 956 try { 957 if (mStackView != null) { 958 mWindowManager.removeView(mStackView); 959 mBubbleData.getOverflow().cleanUpExpandedState(); 960 } 961 if (mLayerView != null) { 962 mWindowManager.removeView(mLayerView); 963 mBubbleData.getOverflow().cleanUpExpandedState(); 964 } 965 } catch (IllegalArgumentException e) { 966 // This means the stack has already been removed - it shouldn't happen, but ignore if it 967 // does, since we wanted it removed anyway. 968 e.printStackTrace(); 969 } 970 } 971 registerBroadcastReceiver()972 private void registerBroadcastReceiver() { 973 IntentFilter filter = new IntentFilter(); 974 filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); 975 filter.addAction(Intent.ACTION_SCREEN_OFF); 976 mContext.registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED); 977 } 978 979 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 980 @Override 981 public void onReceive(Context context, Intent intent) { 982 if (!isStackExpanded()) return; // Nothing to do 983 984 String action = intent.getAction(); 985 String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); 986 boolean validReasonToCollapse = SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason) 987 || SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason) 988 || SYSTEM_DIALOG_REASON_GESTURE_NAV.equals(reason); 989 if ((Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) && validReasonToCollapse) 990 || Intent.ACTION_SCREEN_OFF.equals(action)) { 991 mMainExecutor.execute(() -> collapseStack()); 992 } 993 } 994 }; 995 registerShortcutBroadcastReceiver()996 private void registerShortcutBroadcastReceiver() { 997 IntentFilter shortcutFilter = new IntentFilter(); 998 shortcutFilter.addAction(BubbleShortcutHelper.ACTION_SHOW_BUBBLES); 999 ProtoLog.d(WM_SHELL_BUBBLES, "register broadcast receive for bubbles shortcut"); 1000 mContext.registerReceiver(mShortcutBroadcastReceiver, shortcutFilter, 1001 Context.RECEIVER_NOT_EXPORTED); 1002 } 1003 1004 private final BroadcastReceiver mShortcutBroadcastReceiver = new BroadcastReceiver() { 1005 @Override 1006 public void onReceive(Context context, Intent intent) { 1007 ProtoLog.v(WM_SHELL_BUBBLES, "receive broadcast to show bubbles %s", 1008 intent.getAction()); 1009 if (BubbleShortcutHelper.ACTION_SHOW_BUBBLES.equals(intent.getAction())) { 1010 mMainExecutor.execute(() -> showBubblesFromShortcut()); 1011 } 1012 } 1013 }; 1014 1015 /** 1016 * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been 1017 * added in the meantime. 1018 */ onAllBubblesAnimatedOut()1019 public void onAllBubblesAnimatedOut() { 1020 if (mStackView != null) { 1021 mStackView.setVisibility(INVISIBLE); 1022 removeFromWindowManagerMaybe(); 1023 } else if (mLayerView != null) { 1024 mLayerView.setVisibility(INVISIBLE); 1025 removeFromWindowManagerMaybe(); 1026 } 1027 } 1028 1029 /** 1030 * Records the notification key for any active bubbles. These are used to restore active 1031 * bubbles when the user returns to the foreground. 1032 * 1033 * @param userId the id of the user 1034 */ saveBubbles(@serIdInt int userId)1035 private void saveBubbles(@UserIdInt int userId) { 1036 // First clear any existing keys that might be stored. 1037 mSavedUserBubbleData.remove(userId); 1038 UserBubbleData userBubbleData = new UserBubbleData(); 1039 // Add in all active bubbles for the current user. 1040 for (Bubble bubble : mBubbleData.getBubbles()) { 1041 userBubbleData.add(bubble.getKey(), bubble.showInShade()); 1042 } 1043 mSavedUserBubbleData.put(userId, userBubbleData); 1044 } 1045 1046 /** 1047 * Promotes existing notifications to Bubbles if they were previously bubbles. 1048 * 1049 * @param userId the id of the user 1050 */ restoreBubbles(@serIdInt int userId)1051 private void restoreBubbles(@UserIdInt int userId) { 1052 UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId); 1053 if (savedBubbleData == null) { 1054 // There were no bubbles saved for this used. 1055 return; 1056 } 1057 mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> { 1058 mMainExecutor.execute(() -> { 1059 for (BubbleEntry e : entries) { 1060 if (canLaunchInTaskView(mContext, e)) { 1061 boolean showInShade = savedBubbleData.isShownInShade(e.getKey()); 1062 updateBubble(e, true /* suppressFlyout */, showInShade); 1063 } 1064 } 1065 }); 1066 }); 1067 // Finally, remove the entries for this user now that bubbles are restored. 1068 mSavedUserBubbleData.remove(userId); 1069 } 1070 1071 @Override onThemeChanged()1072 public void onThemeChanged() { 1073 if (mStackView != null) { 1074 mStackView.onThemeChanged(); 1075 } 1076 mBubbleIconFactory = new BubbleIconFactory(mContext, 1077 mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size), 1078 mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size), 1079 mContext.getResources().getColor( 1080 com.android.launcher3.icons.R.color.important_conversation), 1081 mContext.getResources().getDimensionPixelSize( 1082 com.android.internal.R.dimen.importance_ring_stroke_width)); 1083 1084 // Reload each bubble 1085 for (Bubble b : mBubbleData.getBubbles()) { 1086 b.inflate(null /* callback */, 1087 mContext, 1088 mExpandedViewManager, 1089 mBubbleTaskViewFactory, 1090 mBubblePositioner, 1091 mStackView, 1092 mLayerView, 1093 mBubbleIconFactory, 1094 false /* skipInflation */); 1095 } 1096 for (Bubble b : mBubbleData.getOverflowBubbles()) { 1097 b.inflate(null /* callback */, 1098 mContext, 1099 mExpandedViewManager, 1100 mBubbleTaskViewFactory, 1101 mBubblePositioner, 1102 mStackView, 1103 mLayerView, 1104 mBubbleIconFactory, 1105 false /* skipInflation */); 1106 } 1107 } 1108 1109 @Override onConfigurationChanged(Configuration newConfig)1110 public void onConfigurationChanged(Configuration newConfig) { 1111 if (mBubblePositioner != null) { 1112 mBubblePositioner.update(DeviceConfig.create(mContext, mWindowManager)); 1113 } 1114 if (mStackView != null && newConfig != null) { 1115 if (newConfig.densityDpi != mDensityDpi 1116 || !newConfig.windowConfiguration.getBounds().equals(mScreenBounds)) { 1117 mDensityDpi = newConfig.densityDpi; 1118 mScreenBounds.set(newConfig.windowConfiguration.getBounds()); 1119 mBubbleData.onMaxBubblesChanged(); 1120 mBubbleIconFactory = new BubbleIconFactory(mContext, 1121 mContext.getResources().getDimensionPixelSize(R.dimen.bubble_size), 1122 mContext.getResources().getDimensionPixelSize(R.dimen.bubble_badge_size), 1123 mContext.getResources().getColor( 1124 com.android.launcher3.icons.R.color.important_conversation), 1125 mContext.getResources().getDimensionPixelSize( 1126 com.android.internal.R.dimen.importance_ring_stroke_width)); 1127 mStackView.onDisplaySizeChanged(); 1128 } 1129 if (newConfig.fontScale != mFontScale) { 1130 mFontScale = newConfig.fontScale; 1131 mStackView.updateFontScale(); 1132 } 1133 if (newConfig.getLayoutDirection() != mLayoutDirection) { 1134 mLayoutDirection = newConfig.getLayoutDirection(); 1135 mStackView.onLayoutDirectionChanged(mLayoutDirection); 1136 } 1137 Locale newLocale = newConfig.locale; 1138 if (newLocale != null && !newLocale.equals(mLocale)) { 1139 mLocale = newLocale; 1140 mStackView.updateLocale(); 1141 } 1142 } 1143 } 1144 onNotificationPanelExpandedChanged(boolean expanded)1145 private void onNotificationPanelExpandedChanged(boolean expanded) { 1146 if (mStackView != null && mStackView.isExpanded()) { 1147 ProtoLog.d(WM_SHELL_BUBBLES, 1148 "onNotificationPanelExpandedChanged expanded=%b", expanded); 1149 if (expanded) { 1150 mStackView.stopMonitoringSwipeUpGesture(); 1151 } else { 1152 mStackView.startMonitoringSwipeUpGesture(); 1153 } 1154 } 1155 } 1156 setSysuiProxy(Bubbles.SysuiProxy proxy)1157 private void setSysuiProxy(Bubbles.SysuiProxy proxy) { 1158 mSysuiProxy = proxy; 1159 } 1160 1161 @VisibleForTesting setExpandListener(Bubbles.BubbleExpandListener listener)1162 public void setExpandListener(Bubbles.BubbleExpandListener listener) { 1163 mExpandListener = ((isExpanding, key) -> { 1164 if (listener != null) { 1165 listener.onBubbleExpandChanged(isExpanding, key); 1166 } 1167 }); 1168 if (mStackView != null) { 1169 mStackView.setExpandListener(mExpandListener); 1170 } 1171 } 1172 1173 /** 1174 * Whether or not there are bubbles present, regardless of them being visible on the 1175 * screen (e.g. if on AOD). 1176 */ 1177 @VisibleForTesting hasBubbles()1178 public boolean hasBubbles() { 1179 if (mStackView == null && mLayerView == null) { 1180 return false; 1181 } 1182 return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow(); 1183 } 1184 isStackExpanded()1185 public boolean isStackExpanded() { 1186 return mBubbleData.isExpanded(); 1187 } 1188 collapseStack()1189 public void collapseStack() { 1190 mBubbleData.setExpanded(false /* expanded */); 1191 } 1192 1193 /** 1194 * A bubble is being dragged in Launcher. 1195 * Will be called only when bubble bar is expanded. 1196 * 1197 * @param bubbleKey key of the bubble being dragged 1198 */ startBubbleDrag(String bubbleKey)1199 public void startBubbleDrag(String bubbleKey) { 1200 if (mBubbleData.getSelectedBubble() != null) { 1201 mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ false); 1202 } 1203 if (mBubbleStateListener != null) { 1204 boolean overflow = BubbleOverflow.KEY.equals(bubbleKey); 1205 Rect rect = new Rect(); 1206 mBubblePositioner.getBubbleBarExpandedViewBounds(mBubblePositioner.isBubbleBarOnLeft(), 1207 overflow, rect); 1208 BubbleBarUpdate update = new BubbleBarUpdate(); 1209 update.expandedViewDropTargetSize = new Point(rect.width(), rect.height()); 1210 mBubbleStateListener.onBubbleStateChange(update); 1211 } 1212 } 1213 1214 /** 1215 * A bubble is no longer being dragged in Launcher. And was released in given location. 1216 * Will be called only when bubble bar is expanded. 1217 * 1218 * @param location location where bubble was released 1219 * @param topOnScreen top coordinate of the bubble bar on the screen after release 1220 */ stopBubbleDrag(BubbleBarLocation location, int topOnScreen)1221 public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) { 1222 mBubblePositioner.setBubbleBarLocation(location); 1223 mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); 1224 if (mBubbleData.getSelectedBubble() != null) { 1225 mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); 1226 } 1227 } 1228 1229 /** 1230 * A bubble was dragged and is released in dismiss target in Launcher. 1231 * 1232 * @param bubbleKey key of the bubble being dragged to dismiss target 1233 */ dragBubbleToDismiss(String bubbleKey)1234 public void dragBubbleToDismiss(String bubbleKey) { 1235 String selectedBubbleKey = mBubbleData.getSelectedBubbleKey(); 1236 removeBubble(bubbleKey, Bubbles.DISMISS_USER_GESTURE); 1237 if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) { 1238 // We did not remove the selected bubble. Expand it again 1239 mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true); 1240 } 1241 } 1242 1243 /** 1244 * Show bubble bar user education relative to the reference position. 1245 * @param position the reference position in Screen coordinates. 1246 */ showUserEducation(Point position)1247 public void showUserEducation(Point position) { 1248 if (mLayerView == null) return; 1249 mLayerView.showUserEducation(position); 1250 } 1251 1252 @VisibleForTesting isBubbleNotificationSuppressedFromShade(String key, String groupKey)1253 public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { 1254 boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) 1255 && !mBubbleData.getAnyBubbleWithkey(key).showInShade()); 1256 1257 boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey); 1258 boolean isSummary = key.equals(mBubbleData.getSummaryKey(groupKey)); 1259 return (isSummary && isSuppressedSummary) || isSuppressedBubble; 1260 } 1261 1262 /** Promote the provided bubble from the overflow view. */ promoteBubbleFromOverflow(Bubble bubble)1263 public void promoteBubbleFromOverflow(Bubble bubble) { 1264 mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); 1265 ProtoLog.d(WM_SHELL_BUBBLES, "promoteBubbleFromOverflow=%s", bubble.getKey()); 1266 bubble.setInflateSynchronously(mInflateSynchronously); 1267 bubble.setShouldAutoExpand(true); 1268 bubble.markAsAccessedAt(System.currentTimeMillis()); 1269 setIsBubble(bubble, true /* isBubble */); 1270 } 1271 1272 /** 1273 * Expands and selects the provided bubble as long as it already exists in the stack or the 1274 * overflow. 1275 * 1276 * <p>This is used by external callers (launcher). 1277 */ 1278 @VisibleForTesting expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen)1279 public void expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen) { 1280 mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); 1281 1282 if (BubbleOverflow.KEY.equals(key)) { 1283 mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); 1284 mLayerView.showExpandedView(mBubbleData.getOverflow()); 1285 return; 1286 } 1287 1288 Bubble b = mBubbleData.getAnyBubbleWithkey(key); 1289 if (b == null) { 1290 return; 1291 } 1292 if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) { 1293 // already in the stack 1294 mBubbleData.setSelectedBubbleFromLauncher(b); 1295 mLayerView.showExpandedView(b); 1296 } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { 1297 // TODO: (b/271468319) handle overflow 1298 } else { 1299 Log.w(TAG, "didn't add bubble from launcher: " + key); 1300 } 1301 } 1302 1303 /** 1304 * Expands the stack if the selected bubble is present. This is currently used when user 1305 * education view is clicked to expand the selected bubble. 1306 */ expandStackWithSelectedBubble()1307 public void expandStackWithSelectedBubble() { 1308 if (mBubbleData.getSelectedBubble() != null) { 1309 mBubbleData.setExpanded(true); 1310 } 1311 } 1312 1313 /** 1314 * Expands and selects the provided bubble as long as it already exists in the stack or the 1315 * overflow. This is currently used when opening a bubble via clicking on a conversation widget. 1316 */ expandStackAndSelectBubble(Bubble b)1317 public void expandStackAndSelectBubble(Bubble b) { 1318 if (b == null) { 1319 return; 1320 } 1321 if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) { 1322 // already in the stack 1323 mBubbleData.setSelectedBubbleAndExpandStack(b); 1324 } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { 1325 // promote it out of the overflow 1326 promoteBubbleFromOverflow(b); 1327 } 1328 } 1329 1330 /** 1331 * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble 1332 * exists for this entry, and it is able to bubble, a new bubble will be created. 1333 * 1334 * <p>This is the method to use when opening a bubble via a notification or in a state where 1335 * the device might not be unlocked. 1336 * 1337 * @param entry the entry to use for the bubble. 1338 */ expandStackAndSelectBubble(BubbleEntry entry)1339 public void expandStackAndSelectBubble(BubbleEntry entry) { 1340 ProtoLog.d(WM_SHELL_BUBBLES, "opening bubble from notification key=%s mIsStatusBarShade=%b", 1341 entry.getKey(), mIsStatusBarShade); 1342 if (mIsStatusBarShade) { 1343 mNotifEntryToExpandOnShadeUnlock = null; 1344 1345 String key = entry.getKey(); 1346 Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); 1347 if (bubble != null) { 1348 mBubbleData.setSelectedBubbleAndExpandStack(bubble); 1349 } else { 1350 bubble = mBubbleData.getOverflowBubbleWithKey(key); 1351 if (bubble != null) { 1352 promoteBubbleFromOverflow(bubble); 1353 } else if (entry.canBubble()) { 1354 // It can bubble but it's not -- it got aged out of the overflow before it 1355 // was dismissed or opened, make it a bubble again. 1356 setIsBubble(entry, true /* isBubble */, true /* autoExpand */); 1357 } 1358 } 1359 } else { 1360 // Wait until we're unlocked to expand, so that the user can see the expand animation 1361 // and also to work around bugs with expansion animation + shade unlock happening at the 1362 // same time. 1363 mNotifEntryToExpandOnShadeUnlock = entry; 1364 } 1365 } 1366 1367 /** 1368 * Adds or updates a bubble associated with the provided notification entry. 1369 * 1370 * @param notif the notification associated with this bubble. 1371 */ 1372 @VisibleForTesting updateBubble(BubbleEntry notif)1373 public void updateBubble(BubbleEntry notif) { 1374 int bubbleUserId = notif.getStatusBarNotification().getUserId(); 1375 if (isCurrentProfile(bubbleUserId)) { 1376 updateBubble(notif, false /* suppressFlyout */, true /* showInShade */); 1377 } else { 1378 // Skip update, but store it in user bubbles so it gets restored after user switch 1379 mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(), 1380 true /* shownInShade */); 1381 Log.w(TAG, "updateBubble, ignore update for non-active user=" + bubbleUserId 1382 + " currentUser=" + mCurrentUserId); 1383 } 1384 } 1385 1386 /** 1387 * This method has different behavior depending on: 1388 * - if an app bubble exists 1389 * - if an app bubble is expanded 1390 * 1391 * If no app bubble exists, this will add and expand a bubble with the provided intent. The 1392 * intent must be explicit (i.e. include a package name or fully qualified component class name) 1393 * and the activity for it should be resizable. 1394 * 1395 * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is 1396 * expanded, calling this method will collapse it. If the app bubble is not expanded, calling 1397 * this method will expand it. 1398 * 1399 * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses 1400 * the bubble or bubble stack. 1401 * 1402 * Some notes: 1403 * - Only one app bubble is supported at a time, regardless of users. Multi-users support is 1404 * tracked in b/273533235. 1405 * - Calling this method with a different intent than the existing app bubble will do nothing 1406 * 1407 * @param intent the intent to display in the bubble expanded view. 1408 * @param user the {@link UserHandle} of the user to start this activity for. 1409 * @param icon the {@link Icon} to use for the bubble view. 1410 */ showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon)1411 public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { 1412 if (intent == null || intent.getPackage() == null) { 1413 Log.w(TAG, "App bubble failed to show, invalid intent: " + intent 1414 + ((intent != null) ? " with package: " + intent.getPackage() : " ")); 1415 return; 1416 } 1417 1418 String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user); 1419 PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier()); 1420 if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors 1421 1422 Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey); 1423 ProtoLog.d(WM_SHELL_BUBBLES, 1424 "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s " 1425 + "statusBarShade=%s", 1426 appBubbleKey, existingAppBubble, 1427 (mStackView != null ? mStackView.getVisibility() : "null"), 1428 mIsStatusBarShade); 1429 1430 if (existingAppBubble != null) { 1431 BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); 1432 if (isStackExpanded()) { 1433 if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) { 1434 ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey); 1435 // App bubble is expanded, lets collapse 1436 collapseStack(); 1437 } else { 1438 ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey); 1439 // App bubble is not selected, select it 1440 mBubbleData.setSelectedBubble(existingAppBubble); 1441 } 1442 } else { 1443 ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey); 1444 // App bubble is not selected, select it & expand 1445 mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble); 1446 } 1447 } else { 1448 // Check if it exists in the overflow 1449 Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey); 1450 if (b != null) { 1451 // It's in the overflow, so remove it & reinflate 1452 mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); 1453 } else { 1454 // App bubble does not exist, lets add and expand it 1455 b = Bubble.createAppBubble(intent, user, icon, mMainExecutor); 1456 } 1457 ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey); 1458 b.setShouldAutoExpand(true); 1459 inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); 1460 } 1461 } 1462 1463 /** 1464 * Dismiss bubble if it exists and remove it from the stack 1465 */ dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason)1466 public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) { 1467 dismissBubble(bubble.getKey(), reason); 1468 } 1469 1470 /** 1471 * Dismiss bubble with given key if it exists and remove it from the stack 1472 */ dismissBubble(String key, @Bubbles.DismissReason int reason)1473 public void dismissBubble(String key, @Bubbles.DismissReason int reason) { 1474 mBubbleData.dismissBubbleWithKey(key, reason); 1475 } 1476 1477 /** 1478 * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot 1479 * can be access via the supplied {@link SynchronousScreenCaptureListener#getBuffer()} 1480 * asynchronously. 1481 */ getScreenshotExcludingBubble(int displayId, SynchronousScreenCaptureListener screenCaptureListener)1482 public void getScreenshotExcludingBubble(int displayId, 1483 SynchronousScreenCaptureListener screenCaptureListener) { 1484 try { 1485 ScreenCapture.CaptureArgs args = null; 1486 View viewToUse = mStackView != null ? mStackView : mLayerView; 1487 if (viewToUse != null) { 1488 ViewRootImpl viewRoot = viewToUse.getViewRootImpl(); 1489 if (viewRoot != null) { 1490 SurfaceControl bubbleLayer = viewRoot.getSurfaceControl(); 1491 if (bubbleLayer != null) { 1492 args = new ScreenCapture.CaptureArgs.Builder<>() 1493 .setExcludeLayers(new SurfaceControl[] {bubbleLayer}) 1494 .build(); 1495 } 1496 } 1497 } 1498 1499 mWmService.captureDisplay(displayId, args, screenCaptureListener); 1500 } catch (RemoteException e) { 1501 Log.e(TAG, "Failed to capture screenshot"); 1502 } 1503 } 1504 1505 /** Sets the app bubble's taskId which is cached for SysUI. */ setAppBubbleTaskId(String key, int taskId)1506 public void setAppBubbleTaskId(String key, int taskId) { 1507 mImpl.mCachedState.setAppBubbleTaskId(key, taskId); 1508 } 1509 1510 /** 1511 * Fills the overflow bubbles by loading them from disk. 1512 */ loadOverflowBubblesFromDisk()1513 void loadOverflowBubblesFromDisk() { 1514 if (!mOverflowDataLoadNeeded) { 1515 return; 1516 } 1517 mOverflowDataLoadNeeded = false; 1518 List<UserInfo> users = mUserManager.getAliveUsers(); 1519 List<Integer> userIds = users.stream().map(userInfo -> userInfo.id).toList(); 1520 mDataRepository.loadBubbles(mCurrentUserId, userIds, (bubbles) -> { 1521 bubbles.forEach(bubble -> { 1522 if (mBubbleData.hasAnyBubbleWithKey(bubble.getKey())) { 1523 // if the bubble is already active, there's no need to push it to overflow 1524 return; 1525 } 1526 bubble.inflate( 1527 (b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble), 1528 mContext, 1529 mExpandedViewManager, 1530 mBubbleTaskViewFactory, 1531 mBubblePositioner, 1532 mStackView, 1533 mLayerView, 1534 mBubbleIconFactory, 1535 true /* skipInflation */); 1536 }); 1537 return null; 1538 }); 1539 } 1540 setUpBubbleViewsForMode()1541 void setUpBubbleViewsForMode() { 1542 mBubbleViewCallback = isShowingAsBubbleBar() 1543 ? mBubbleBarViewCallback 1544 : mBubbleStackViewCallback; 1545 1546 // reset the overflow so that it can be re-added later if needed. 1547 if (mStackView != null) { 1548 mStackView.resetOverflowView(); 1549 mStackView.removeAllViews(); 1550 } 1551 // cleanup existing bubble views so they can be recreated later if needed, but retain 1552 // TaskView. 1553 mBubbleData.getBubbles().forEach(b -> b.cleanupViews(/* cleanupTaskView= */ false)); 1554 1555 // remove the current bubble container from window manager, null it out, and create a new 1556 // container based on the current mode. 1557 removeFromWindowManagerMaybe(); 1558 mLayerView = null; 1559 mStackView = null; 1560 1561 if (!mBubbleData.hasBubbles()) { 1562 // if there are no bubbles, don't create the stack or layer views. they will be created 1563 // later when the first bubble is added. 1564 return; 1565 } 1566 1567 ensureBubbleViewsAndWindowCreated(); 1568 1569 // inflate bubble views 1570 BubbleViewInfoTask.Callback callback = null; 1571 if (!isShowingAsBubbleBar()) { 1572 callback = b -> { 1573 if (mStackView != null) { 1574 mStackView.addBubble(b); 1575 mStackView.setSelectedBubble(b); 1576 } else { 1577 Log.w(TAG, "Tried to add a bubble to the stack but the stack is null"); 1578 } 1579 }; 1580 } else if (mBubbleData.isExpanded() && mBubbleData.getSelectedBubble() != null) { 1581 callback = b -> { 1582 if (b.getKey().equals(mBubbleData.getSelectedBubbleKey())) { 1583 mLayerView.showExpandedView(b); 1584 } 1585 }; 1586 } 1587 for (int i = mBubbleData.getBubbles().size() - 1; i >= 0; i--) { 1588 Bubble bubble = mBubbleData.getBubbles().get(i); 1589 bubble.inflate(callback, 1590 mContext, 1591 mExpandedViewManager, 1592 mBubbleTaskViewFactory, 1593 mBubblePositioner, 1594 mStackView, 1595 mLayerView, 1596 mBubbleIconFactory, 1597 false /* skipInflation */); 1598 } 1599 } 1600 1601 /** 1602 * Adds or updates a bubble associated with the provided notification entry. 1603 * 1604 * @param notif the notification associated with this bubble. 1605 * @param suppressFlyout this bubble suppress flyout or not. 1606 * @param showInShade this bubble show in shade or not. 1607 */ 1608 @VisibleForTesting updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade)1609 public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { 1610 // If this is an interruptive notif, mark that it's interrupted 1611 mSysuiProxy.setNotificationInterruption(notif.getKey()); 1612 boolean isNonInterruptiveNotExpanding = !notif.getRanking().isTextChanged() 1613 && (notif.getBubbleMetadata() != null 1614 && !notif.getBubbleMetadata().getAutoExpandBubble()); 1615 if (isNonInterruptiveNotExpanding 1616 && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { 1617 // Update the bubble but don't promote it out of overflow 1618 Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); 1619 if (notif.isBubble()) { 1620 notif.setFlagBubble(false); 1621 } 1622 updateNotNotifyingEntry(b, notif, showInShade); 1623 } else if (mBubbleData.hasAnyBubbleWithKey(notif.getKey()) 1624 && isNonInterruptiveNotExpanding) { 1625 Bubble b = mBubbleData.getAnyBubbleWithkey(notif.getKey()); 1626 if (b != null) { 1627 updateNotNotifyingEntry(b, notif, showInShade); 1628 } 1629 } else if (mBubbleData.isSuppressedWithLocusId(notif.getLocusId())) { 1630 // Update the bubble but don't promote it out of overflow 1631 Bubble b = mBubbleData.getSuppressedBubbleWithKey(notif.getKey()); 1632 if (b != null) { 1633 updateNotNotifyingEntry(b, notif, showInShade); 1634 } 1635 } else { 1636 Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); 1637 if (notif.shouldSuppressNotificationList()) { 1638 // If we're suppressing notifs for DND, we don't want the bubbles to randomly 1639 // expand when DND turns off so flip the flag. 1640 if (bubble.shouldAutoExpand()) { 1641 bubble.setShouldAutoExpand(false); 1642 } 1643 mImpl.mCachedState.updateBubbleSuppressedState(bubble); 1644 } else { 1645 inflateAndAdd(bubble, suppressFlyout, showInShade); 1646 } 1647 } 1648 } 1649 updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade)1650 void updateNotNotifyingEntry(Bubble b, BubbleEntry entry, boolean showInShade) { 1651 boolean showInShadeBefore = b.showInShade(); 1652 boolean isBubbleSelected = Objects.equals(b, mBubbleData.getSelectedBubble()); 1653 boolean isBubbleExpandedAndSelected = isStackExpanded() && isBubbleSelected; 1654 b.setEntry(entry); 1655 boolean suppress = isBubbleExpandedAndSelected || !showInShade || !b.showInShade(); 1656 b.setSuppressNotification(suppress); 1657 b.setShowDot(!isBubbleExpandedAndSelected); 1658 if (showInShadeBefore != b.showInShade()) { 1659 mImpl.mCachedState.updateBubbleSuppressedState(b); 1660 } 1661 } 1662 1663 @VisibleForTesting inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade)1664 public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { 1665 // Lazy init stack view when a bubble is created 1666 ensureBubbleViewsAndWindowCreated(); 1667 bubble.setInflateSynchronously(mInflateSynchronously); 1668 bubble.inflate( 1669 b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), 1670 mContext, 1671 mExpandedViewManager, 1672 mBubbleTaskViewFactory, 1673 mBubblePositioner, 1674 mStackView, 1675 mLayerView, 1676 mBubbleIconFactory, 1677 false /* skipInflation */); 1678 } 1679 1680 /** 1681 * Removes the bubble with the given key. 1682 * <p> 1683 * Must be called from the main thread. 1684 */ 1685 @MainThread removeBubble(String key, int reason)1686 public void removeBubble(String key, int reason) { 1687 if (mBubbleData.hasAnyBubbleWithKey(key)) { 1688 mBubbleData.dismissBubbleWithKey(key, reason); 1689 } 1690 } 1691 1692 /** 1693 * Removes all the bubbles. 1694 * <p> 1695 * Must be called from the main thread. 1696 */ 1697 @VisibleForTesting 1698 @MainThread removeAllBubbles(@ubbles.DismissReason int reason)1699 public void removeAllBubbles(@Bubbles.DismissReason int reason) { 1700 mBubbleData.dismissAll(reason); 1701 } 1702 onEntryAdded(BubbleEntry entry)1703 private void onEntryAdded(BubbleEntry entry) { 1704 if (canLaunchInTaskView(mContext, entry)) { 1705 updateBubble(entry); 1706 } 1707 } 1708 1709 @VisibleForTesting onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem)1710 public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) { 1711 if (!fromSystem) { 1712 return; 1713 } 1714 // shouldBubbleUp checks canBubble & for bubble metadata 1715 boolean shouldBubble = shouldBubbleUp && canLaunchInTaskView(mContext, entry); 1716 if (!shouldBubble && mBubbleData.hasAnyBubbleWithKey(entry.getKey())) { 1717 // It was previously a bubble but no longer a bubble -- lets remove it 1718 removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE); 1719 } else if (shouldBubble && entry.isBubble()) { 1720 updateBubble(entry); 1721 } 1722 } 1723 onEntryRemoved(BubbleEntry entry)1724 private void onEntryRemoved(BubbleEntry entry) { 1725 if (isSummaryOfBubbles(entry)) { 1726 final String groupKey = entry.getStatusBarNotification().getGroupKey(); 1727 mBubbleData.removeSuppressedSummary(groupKey); 1728 1729 // Remove any associated bubble children with the summary 1730 final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey); 1731 for (int i = 0; i < bubbleChildren.size(); i++) { 1732 removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED); 1733 } 1734 } else { 1735 removeBubble(entry.getKey(), DISMISS_NOTIF_CANCEL); 1736 } 1737 } 1738 1739 @VisibleForTesting onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)1740 public void onRankingUpdated(RankingMap rankingMap, 1741 HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { 1742 if (mTmpRanking == null) { 1743 mTmpRanking = new NotificationListenerService.Ranking(); 1744 } 1745 String[] orderedKeys = rankingMap.getOrderedKeys(); 1746 for (int i = 0; i < orderedKeys.length; i++) { 1747 String key = orderedKeys[i]; 1748 Pair<BubbleEntry, Boolean> entryData = entryDataByKey.get(key); 1749 BubbleEntry entry = entryData.first; 1750 boolean shouldBubbleUp = entryData.second; 1751 if (entry != null && !isCurrentProfile( 1752 entry.getStatusBarNotification().getUser().getIdentifier())) { 1753 return; 1754 } 1755 if (entry != null && (entry.shouldSuppressNotificationList() 1756 || entry.getRanking().isSuspended())) { 1757 shouldBubbleUp = false; 1758 } 1759 rankingMap.getRanking(key, mTmpRanking); 1760 boolean isActiveOrInOverflow = mBubbleData.hasAnyBubbleWithKey(key); 1761 boolean isActive = mBubbleData.hasBubbleInStackWithKey(key); 1762 if (isActiveOrInOverflow && !mTmpRanking.canBubble()) { 1763 // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. 1764 // This means that the app or channel's ability to bubble has been revoked. 1765 mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); 1766 } else if (isActiveOrInOverflow && !shouldBubbleUp) { 1767 // If this entry is allowed to bubble, but cannot currently bubble up or is 1768 // suspended, dismiss it. This happens when DND is enabled and configured to hide 1769 // bubbles, or focus mode is enabled and the app is designated as distracting. 1770 // Dismissing with the reason DISMISS_NO_BUBBLE_UP will retain the underlying 1771 // notification, so that the bubble will be re-created if shouldBubbleUp returns 1772 // true. 1773 mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); 1774 } else if (entry != null && mTmpRanking.isBubble() && !isActiveOrInOverflow) { 1775 entry.setFlagBubble(true); 1776 onEntryUpdated(entry, shouldBubbleUp, /* fromSystem= */ true); 1777 } 1778 } 1779 } 1780 1781 @VisibleForTesting onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)1782 public void onNotificationChannelModified(String pkg, UserHandle user, 1783 NotificationChannel channel, int modificationType) { 1784 // Only query overflow bubbles here because active bubbles will have an active notification 1785 // and channel changes we care about would result in a ranking update. 1786 List<Bubble> overflowBubbles = new ArrayList<>(mBubbleData.getOverflowBubbles()); 1787 for (int i = 0; i < overflowBubbles.size(); i++) { 1788 Bubble b = overflowBubbles.get(i); 1789 if (Objects.equals(b.getShortcutId(), channel.getConversationId()) 1790 && b.getPackageName().equals(pkg) 1791 && b.getUser().getIdentifier() == user.getIdentifier()) { 1792 if (!channel.canBubble() || channel.isDeleted()) { 1793 mBubbleData.dismissBubbleWithKey(b.getKey(), DISMISS_NO_LONGER_BUBBLE); 1794 } 1795 } 1796 } 1797 } 1798 1799 /** 1800 * Retrieves any bubbles that are part of the notification group represented by the provided 1801 * group key. 1802 */ getBubblesInGroup(@ullable String groupKey)1803 private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { 1804 ArrayList<Bubble> bubbleChildren = new ArrayList<>(); 1805 if (groupKey == null) { 1806 return bubbleChildren; 1807 } 1808 for (Bubble bubble : mBubbleData.getBubbles()) { 1809 if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) { 1810 bubbleChildren.add(bubble); 1811 } 1812 } 1813 return bubbleChildren; 1814 } 1815 setIsBubble(@onNull final BubbleEntry entry, final boolean isBubble, final boolean autoExpand)1816 private void setIsBubble(@NonNull final BubbleEntry entry, final boolean isBubble, 1817 final boolean autoExpand) { 1818 Objects.requireNonNull(entry); 1819 entry.setFlagBubble(isBubble); 1820 try { 1821 int flags = 0; 1822 if (autoExpand) { 1823 flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 1824 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; 1825 } 1826 mBarService.onNotificationBubbleChanged(entry.getKey(), isBubble, flags); 1827 } catch (RemoteException e) { 1828 // Bad things have happened 1829 } 1830 } 1831 setIsBubble(@onNull final Bubble b, final boolean isBubble)1832 private void setIsBubble(@NonNull final Bubble b, final boolean isBubble) { 1833 Objects.requireNonNull(b); 1834 b.setIsBubble(isBubble); 1835 mSysuiProxy.getPendingOrActiveEntry(b.getKey(), (entry) -> { 1836 mMainExecutor.execute(() -> { 1837 if (entry != null) { 1838 // Updating the entry to be a bubble will trigger our normal update flow 1839 setIsBubble(entry, isBubble, b.shouldAutoExpand()); 1840 } else if (isBubble) { 1841 // If bubble doesn't exist, it's a persisted bubble so we need to add it to the 1842 // stack ourselves 1843 Bubble bubble = mBubbleData.getOrCreateBubble(null, b /* persistedBubble */); 1844 inflateAndAdd(bubble, bubble.shouldAutoExpand() /* suppressFlyout */, 1845 !bubble.shouldAutoExpand() /* showInShade */); 1846 } 1847 }); 1848 }); 1849 } 1850 1851 /** When bubbles are floating, this will be used to notify the floating views. */ 1852 private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() { 1853 @Override 1854 public void removeBubble(Bubble removedBubble) { 1855 if (mStackView != null) { 1856 mStackView.removeBubble(removedBubble); 1857 } 1858 } 1859 1860 @Override 1861 public void addBubble(Bubble addedBubble) { 1862 if (mStackView != null) { 1863 mStackView.addBubble(addedBubble); 1864 } 1865 } 1866 1867 @Override 1868 public void updateBubble(Bubble updatedBubble) { 1869 if (mStackView != null) { 1870 mStackView.updateBubble(updatedBubble); 1871 } 1872 } 1873 1874 @Override 1875 public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) { 1876 if (mStackView != null) { 1877 mStackView.updateBubbleOrder(bubbleOrder, updatePointer); 1878 } 1879 } 1880 1881 @Override 1882 public void suppressionChanged(Bubble bubble, boolean isSuppressed) { 1883 if (mStackView != null) { 1884 mStackView.setBubbleSuppressed(bubble, isSuppressed); 1885 } 1886 } 1887 1888 @Override 1889 public void expansionChanged(boolean isExpanded) { 1890 if (mStackView != null) { 1891 mStackView.setExpanded(isExpanded); 1892 } 1893 } 1894 1895 @Override 1896 public void selectionChanged(BubbleViewProvider selectedBubble) { 1897 if (mStackView != null) { 1898 mStackView.setSelectedBubble(selectedBubble); 1899 } 1900 1901 } 1902 1903 @Override 1904 public void bubbleOverflowChanged(boolean hasBubbles) { 1905 if (Flags.enableOptionalBubbleOverflow()) { 1906 if (mStackView != null) { 1907 mStackView.showOverflow(hasBubbles); 1908 } 1909 } 1910 } 1911 }; 1912 1913 /** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */ 1914 private final BubbleViewCallback mBubbleBarViewCallback = new BubbleViewCallback() { 1915 @Override 1916 public void removeBubble(Bubble removedBubble) { 1917 if (mLayerView != null) { 1918 mLayerView.removeBubble(removedBubble, () -> { 1919 if (!mBubbleData.hasBubbles() && !isStackExpanded()) { 1920 mLayerView.setVisibility(INVISIBLE); 1921 removeFromWindowManagerMaybe(); 1922 } 1923 }); 1924 } 1925 } 1926 1927 @Override 1928 public void addBubble(Bubble addedBubble) { 1929 // Nothing to do for adds, these are handled by launcher / in the bubble bar. 1930 } 1931 1932 @Override 1933 public void updateBubble(Bubble updatedBubble) { 1934 // Nothing to do for updates, these are handled by launcher / in the bubble bar. 1935 } 1936 1937 @Override 1938 public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) { 1939 // Nothing to do for order changes, these are handled by launcher / in the bubble bar. 1940 } 1941 1942 @Override 1943 public void bubbleOverflowChanged(boolean hasBubbles) { 1944 // Nothing to do for our views, handled by launcher / in the bubble bar. 1945 } 1946 1947 @Override 1948 public void suppressionChanged(Bubble bubble, boolean isSuppressed) { 1949 if (mLayerView != null) { 1950 // TODO (b/273316505) handle suppression changes, although might not need to 1951 // to do anything on the layerview side for this... 1952 } 1953 } 1954 1955 @Override 1956 public void expansionChanged(boolean isExpanded) { 1957 if (mLayerView != null) { 1958 if (!isExpanded) { 1959 mLayerView.collapse(); 1960 } else { 1961 BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); 1962 if (selectedBubble != null) { 1963 mLayerView.showExpandedView(selectedBubble); 1964 } 1965 } 1966 } 1967 } 1968 1969 @Override 1970 public void selectionChanged(BubbleViewProvider selectedBubble) { 1971 // Only need to update the layer view if we're currently expanded for selection changes. 1972 if (mLayerView != null && isStackExpanded()) { 1973 mLayerView.showExpandedView(selectedBubble); 1974 } 1975 } 1976 }; 1977 1978 @SuppressWarnings("FieldCanBeLocal") 1979 private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { 1980 1981 @Override 1982 public void applyUpdate(BubbleData.Update update) { 1983 ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:" 1984 + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b" 1985 + " expanded=%b selectionChanged=%b selected=%s" 1986 + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b", 1987 update.addedBubble != null ? update.addedBubble.getKey() : "null", 1988 !update.removedBubbles.isEmpty(), 1989 update.updatedBubble != null ? update.updatedBubble.getKey() : "null", 1990 update.orderChanged, update.expandedChanged, update.expanded, 1991 update.selectionChanged, 1992 update.selectedBubble != null ? update.selectedBubble.getKey() : "null", 1993 update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null", 1994 update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null", 1995 update.shouldShowEducation, update.showOverflowChanged); 1996 1997 ensureBubbleViewsAndWindowCreated(); 1998 1999 // Lazy load overflow bubbles from disk 2000 loadOverflowBubblesFromDisk(); 2001 2002 if (update.showOverflowChanged) { 2003 mBubbleViewCallback.bubbleOverflowChanged(!update.overflowBubbles.isEmpty()); 2004 } 2005 2006 // If bubbles in the overflow have a dot, make sure the overflow shows a dot 2007 updateOverflowButtonDot(); 2008 2009 // Update bubbles in overflow. 2010 if (mOverflowListener != null) { 2011 mOverflowListener.applyUpdate(update); 2012 } 2013 2014 // Do removals, if any. 2015 ArrayList<Pair<Bubble, Integer>> removedBubbles = 2016 new ArrayList<>(update.removedBubbles); 2017 ArrayList<Bubble> bubblesToBeRemovedFromRepository = new ArrayList<>(); 2018 for (Pair<Bubble, Integer> removed : removedBubbles) { 2019 final Bubble bubble = removed.first; 2020 @Bubbles.DismissReason final int reason = removed.second; 2021 2022 mBubbleViewCallback.removeBubble(bubble); 2023 2024 // Leave the notification in place if we're dismissing due to user switching, or 2025 // because DND is suppressing the bubble. In both of those cases, we need to be able 2026 // to restore the bubble from the notification later. 2027 if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) { 2028 continue; 2029 } 2030 if (reason == DISMISS_NOTIF_CANCEL 2031 || reason == DISMISS_SHORTCUT_REMOVED) { 2032 bubblesToBeRemovedFromRepository.add(bubble); 2033 } 2034 if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 2035 if (!mBubbleData.hasOverflowBubbleWithKey(bubble.getKey()) 2036 && (!bubble.showInShade() 2037 || reason == DISMISS_NOTIF_CANCEL 2038 || reason == DISMISS_GROUP_CANCELLED)) { 2039 // The bubble is now gone & the notification is hidden from the shade, so 2040 // time to actually remove it 2041 mSysuiProxy.notifyRemoveNotification(bubble.getKey(), REASON_CANCEL); 2042 } else { 2043 if (bubble.isBubble()) { 2044 setIsBubble(bubble, false /* isBubble */); 2045 } 2046 mSysuiProxy.updateNotificationBubbleButton(bubble.getKey()); 2047 } 2048 } 2049 } 2050 mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); 2051 2052 if (update.addedBubble != null) { 2053 mDataRepository.addBubble(mCurrentUserId, update.addedBubble); 2054 mBubbleViewCallback.addBubble(update.addedBubble); 2055 } 2056 2057 if (update.updatedBubble != null) { 2058 mBubbleViewCallback.updateBubble(update.updatedBubble); 2059 } 2060 2061 if (update.suppressedBubble != null) { 2062 mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true); 2063 } 2064 2065 if (update.unsuppressedBubble != null) { 2066 mBubbleViewCallback.suppressionChanged(update.unsuppressedBubble, false); 2067 } 2068 2069 boolean collapseStack = update.expandedChanged && !update.expanded; 2070 2071 // At this point, the correct bubbles are inflated in the stack. 2072 // Make sure the order in bubble data is reflected in bubble row. 2073 if (update.orderChanged) { 2074 mDataRepository.addBubbles(mCurrentUserId, update.bubbles); 2075 // if the stack is going to be collapsed, do not update pointer position 2076 // after reordering 2077 mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack); 2078 } 2079 2080 if (collapseStack) { 2081 mBubbleViewCallback.expansionChanged(/* expanded= */ false); 2082 mSysuiProxy.requestNotificationShadeTopUi(false, TAG); 2083 } 2084 2085 if (update.selectionChanged) { 2086 mBubbleViewCallback.selectionChanged(update.selectedBubble); 2087 } 2088 2089 // Expanding? Apply this last. 2090 if (update.expandedChanged && update.expanded) { 2091 mBubbleViewCallback.expansionChanged(/* expanded= */ true); 2092 mSysuiProxy.requestNotificationShadeTopUi(true, TAG); 2093 } 2094 2095 mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate"); 2096 updateBubbleViews(); 2097 2098 // Update the cached state for queries from SysUI 2099 mImpl.mCachedState.update(update); 2100 2101 if (isShowingAsBubbleBar()) { 2102 BubbleBarUpdate bubbleBarUpdate = update.toBubbleBarUpdate(); 2103 // Some updates aren't relevant to the bubble bar so check first. 2104 if (bubbleBarUpdate.anythingChanged()) { 2105 mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); 2106 } 2107 } 2108 } 2109 }; 2110 updateOverflowButtonDot()2111 private void updateOverflowButtonDot() { 2112 BubbleOverflow overflow = mBubbleData.getOverflow(); 2113 if (overflow == null) return; 2114 2115 for (Bubble b : mBubbleData.getOverflowBubbles()) { 2116 if (b.showDot()) { 2117 overflow.setShowDot(true); 2118 return; 2119 } 2120 } 2121 overflow.setShowDot(false); 2122 } 2123 handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)2124 private boolean handleDismissalInterception(BubbleEntry entry, 2125 @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { 2126 if (isSummaryOfBubbles(entry)) { 2127 handleSummaryDismissalInterception(entry, children, removeCallback); 2128 } else { 2129 Bubble bubble = mBubbleData.getBubbleInStackWithKey(entry.getKey()); 2130 if (bubble == null || !entry.isBubble()) { 2131 bubble = mBubbleData.getOverflowBubbleWithKey(entry.getKey()); 2132 } 2133 if (bubble == null) { 2134 return false; 2135 } 2136 bubble.setSuppressNotification(true); 2137 bubble.setShowDot(false /* show */); 2138 } 2139 // Update the shade 2140 mSysuiProxy.notifyInvalidateNotifications("BubbleController.handleDismissalInterception"); 2141 return true; 2142 } 2143 isSummaryOfBubbles(BubbleEntry entry)2144 private boolean isSummaryOfBubbles(BubbleEntry entry) { 2145 String groupKey = entry.getStatusBarNotification().getGroupKey(); 2146 ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey); 2147 boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey) 2148 && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey()); 2149 boolean isSummary = entry.getStatusBarNotification().getNotification().isGroupSummary(); 2150 return (isSuppressedSummary || isSummary) && !bubbleChildren.isEmpty(); 2151 } 2152 handleSummaryDismissalInterception( BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback)2153 private void handleSummaryDismissalInterception( 2154 BubbleEntry summary, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { 2155 if (children != null) { 2156 for (int i = 0; i < children.size(); i++) { 2157 BubbleEntry child = children.get(i); 2158 if (mBubbleData.hasAnyBubbleWithKey(child.getKey())) { 2159 // Suppress the bubbled child 2160 // As far as group manager is concerned, once a child is no longer shown 2161 // in the shade, it is essentially removed. 2162 Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey()); 2163 if (bubbleChild != null) { 2164 bubbleChild.setSuppressNotification(true); 2165 bubbleChild.setShowDot(false /* show */); 2166 } 2167 } else { 2168 // non-bubbled children can be removed 2169 removeCallback.accept(i); 2170 } 2171 } 2172 } 2173 2174 // And since all children are removed, remove the summary. 2175 removeCallback.accept(-1); 2176 2177 // TODO: (b/145659174) remove references to mSuppressedGroupKeys once fully migrated 2178 mBubbleData.addSummaryToSuppress(summary.getStatusBarNotification().getGroupKey(), 2179 summary.getKey()); 2180 } 2181 2182 /** 2183 * Updates the visibility of the bubbles based on current state. 2184 * Does not un-bubble, just hides or un-hides the views themselves. 2185 * 2186 * Updates view description for TalkBack focus. 2187 * Updates bubbles' icon views clickable states (when floating). 2188 */ updateBubbleViews()2189 public void updateBubbleViews() { 2190 if (mStackView == null && mLayerView == null) { 2191 return; 2192 } 2193 ProtoLog.v(WM_SHELL_BUBBLES, "updateBubbleViews mIsStatusBarShade=%s hasBubbles=%s", 2194 mIsStatusBarShade, hasBubbles()); 2195 if (!mIsStatusBarShade) { 2196 // Bubbles don't appear when the device is locked. 2197 if (mStackView != null) { 2198 mStackView.setVisibility(INVISIBLE); 2199 } 2200 if (mLayerView != null) { 2201 mLayerView.setVisibility(INVISIBLE); 2202 } 2203 } else if (hasBubbles()) { 2204 // If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the 2205 // stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate 2206 // out. 2207 if (mStackView != null) { 2208 mStackView.setVisibility(VISIBLE); 2209 } 2210 if (mLayerView != null) { 2211 mLayerView.setVisibility(VISIBLE); 2212 } 2213 } 2214 2215 if (mStackView != null) { 2216 mStackView.updateContentDescription(); 2217 mStackView.updateBubblesAcessibillityStates(); 2218 } else if (mLayerView != null) { 2219 // TODO(b/273313561): handle a11y for BubbleBarLayerView 2220 } 2221 } 2222 2223 /** 2224 * Returns whether the stack is animating or not. 2225 */ isStackAnimating()2226 public boolean isStackAnimating() { 2227 return mStackView != null 2228 && (mStackView.isExpansionAnimating() 2229 || mStackView.isSwitchAnimating()); 2230 } 2231 2232 @VisibleForTesting 2233 @Nullable getStackView()2234 public BubbleStackView getStackView() { 2235 return mStackView; 2236 } 2237 2238 @VisibleForTesting 2239 @Nullable getLayerView()2240 public BubbleBarLayerView getLayerView() { 2241 return mLayerView; 2242 } 2243 2244 /** 2245 * Check if notification panel is in an expanded state. 2246 * Makes a call to System UI process and delivers the result via {@code callback} on the 2247 * WM Shell main thread. 2248 * 2249 * @param callback callback that has the result of notification panel expanded state 2250 */ isNotificationPanelExpanded(Consumer<Boolean> callback)2251 public void isNotificationPanelExpanded(Consumer<Boolean> callback) { 2252 mSysuiProxy.isNotificationPanelExpand(expanded -> 2253 mMainExecutor.execute(() -> callback.accept(expanded))); 2254 } 2255 2256 /** 2257 * Show bubbles UI when triggered via shortcut. 2258 * 2259 * <p>When there are bubbles visible, expands the top-most bubble. When there are no bubbles 2260 * visible, opens the bubbles overflow UI. 2261 */ showBubblesFromShortcut()2262 public void showBubblesFromShortcut() { 2263 if (isStackExpanded()) { 2264 ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: stack visible, skip"); 2265 return; 2266 } 2267 if (mBubbleData.getSelectedBubble() != null) { 2268 ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: open selected bubble"); 2269 expandStackWithSelectedBubble(); 2270 return; 2271 } 2272 BubbleViewProvider bubbleToSelect = CollectionUtils.firstOrNull(mBubbleData.getBubbles()); 2273 if (bubbleToSelect == null) { 2274 ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: no bubbles"); 2275 // make sure overflow bubbles are loaded 2276 loadOverflowBubblesFromDisk(); 2277 bubbleToSelect = mBubbleData.getOverflow(); 2278 } 2279 ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: select and open %s", 2280 bubbleToSelect.getKey()); 2281 mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect); 2282 } 2283 2284 /** 2285 * Description of current bubble state. 2286 */ dump(PrintWriter pw, String prefix)2287 private void dump(PrintWriter pw, String prefix) { 2288 pw.print(prefix); pw.println("BubbleController state:"); 2289 pw.print(prefix); pw.println(" currentUserId= " + mCurrentUserId); 2290 pw.print(prefix); pw.println(" isStatusBarShade= " + mIsStatusBarShade); 2291 pw.print(prefix); pw.println(" isShowingAsBubbleBar= " + isShowingAsBubbleBar()); 2292 pw.print(prefix); pw.println(" isImeVisible= " + mBubblePositioner.isImeVisible()); 2293 pw.println(); 2294 2295 mBubbleData.dump(pw); 2296 pw.println(); 2297 2298 if (mStackView != null) { 2299 mStackView.dump(pw); 2300 } 2301 pw.println(); 2302 2303 mImpl.mCachedState.dump(pw); 2304 } 2305 2306 /** 2307 * Whether an intent is properly configured to display in a 2308 * {@link TaskView}. 2309 * 2310 * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically 2311 * that should filter out any invalid bubbles, but should protect SysUI side just in case. 2312 * 2313 * @param context the context to use. 2314 * @param entry the entry to bubble. 2315 */ canLaunchInTaskView(Context context, BubbleEntry entry)2316 static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { 2317 PendingIntent intent = entry.getBubbleMetadata() != null 2318 ? entry.getBubbleMetadata().getIntent() 2319 : null; 2320 if (entry.getBubbleMetadata() != null 2321 && entry.getBubbleMetadata().getShortcutId() != null) { 2322 return true; 2323 } 2324 if (intent == null) { 2325 Log.w(TAG, "Unable to create bubble -- no intent: " + entry.getKey()); 2326 return false; 2327 } 2328 PackageManager packageManager = getPackageManagerForUser( 2329 context, entry.getStatusBarNotification().getUser().getIdentifier()); 2330 return isResizableActivity(intent.getIntent(), packageManager, entry.getKey()); 2331 } 2332 isResizableActivity(Intent intent, PackageManager packageManager, String key)2333 static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) { 2334 if (intent == null) { 2335 Log.w(TAG, "Unable to send as bubble: " + key + " null intent"); 2336 return false; 2337 } 2338 ActivityInfo info = intent.resolveActivityInfo(packageManager, 0); 2339 if (info == null) { 2340 Log.w(TAG, "Unable to send as bubble: " + key 2341 + " couldn't find activity info for intent: " + intent); 2342 return false; 2343 } 2344 if (!ActivityInfo.isResizeableMode(info.resizeMode)) { 2345 Log.w(TAG, "Unable to send as bubble: " + key 2346 + " activity is not resizable for intent: " + intent); 2347 return false; 2348 } 2349 return true; 2350 } 2351 getPackageManagerForUser(Context context, int userId)2352 static PackageManager getPackageManagerForUser(Context context, int userId) { 2353 Context contextForUser = context; 2354 // UserHandle defines special userId as negative values, e.g. USER_ALL 2355 if (userId >= 0) { 2356 try { 2357 // Create a context for the correct user so if a package isn't installed 2358 // for user 0 we can still load information about the package. 2359 contextForUser = 2360 context.createPackageContextAsUser(context.getPackageName(), 2361 Context.CONTEXT_RESTRICTED, 2362 new UserHandle(userId)); 2363 } catch (PackageManager.NameNotFoundException e) { 2364 // Shouldn't fail to find the package name for system ui. 2365 } 2366 } 2367 return contextForUser.getPackageManager(); 2368 } 2369 2370 /** PinnedStackListener that dispatches IME visibility updates to the stack. */ 2371 //TODO(b/170442945): Better way to do this / insets listener? 2372 private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener { 2373 @Override onImeVisibilityChanged(boolean imeVisible, int imeHeight)2374 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { 2375 mBubblePositioner.setImeVisible(imeVisible, imeHeight); 2376 if (mStackView != null) { 2377 mStackView.setImeVisible(imeVisible); 2378 } 2379 } 2380 } 2381 2382 /** 2383 * The interface for calls from outside the host process. 2384 */ 2385 @BinderThread 2386 private class IBubblesImpl extends IBubbles.Stub implements ExternalInterfaceBinder { 2387 private BubbleController mController; 2388 private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener; 2389 private final Bubbles.BubbleStateListener mBubbleListener = 2390 new Bubbles.BubbleStateListener() { 2391 @Override 2392 public void onBubbleStateChange(BubbleBarUpdate update) { 2393 Bundle b = new Bundle(); 2394 b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); 2395 b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); 2396 mListener.call(l -> l.onBubbleStateChange(b)); 2397 } 2398 2399 @Override 2400 public void animateBubbleBarLocation(BubbleBarLocation location) { 2401 mListener.call(l -> l.animateBubbleBarLocation(location)); 2402 } 2403 }; 2404 IBubblesImpl(BubbleController controller)2405 IBubblesImpl(BubbleController controller) { 2406 mController = controller; 2407 mListener = new SingleInstanceRemoteListener<>(mController, 2408 c -> c.registerBubbleStateListener(mBubbleListener), 2409 c -> c.unregisterBubbleStateListener()); 2410 } 2411 2412 /** 2413 * Invalidates this instance, preventing future calls from updating the controller. 2414 */ 2415 @Override invalidate()2416 public void invalidate() { 2417 mController = null; 2418 // Unregister the listeners to ensure any binder death recipients are unlinked 2419 mListener.unregister(); 2420 } 2421 2422 @Override registerBubbleListener(IBubblesListener listener)2423 public void registerBubbleListener(IBubblesListener listener) { 2424 mMainExecutor.execute(() -> mListener.register(listener)); 2425 } 2426 2427 @Override unregisterBubbleListener(IBubblesListener listener)2428 public void unregisterBubbleListener(IBubblesListener listener) { 2429 mMainExecutor.execute(mListener::unregister); 2430 } 2431 2432 @Override showBubble(String key, int topOnScreen)2433 public void showBubble(String key, int topOnScreen) { 2434 mMainExecutor.execute( 2435 () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen)); 2436 } 2437 2438 @Override removeAllBubbles()2439 public void removeAllBubbles() { 2440 mMainExecutor.execute(() -> mController.removeAllBubbles(Bubbles.DISMISS_USER_GESTURE)); 2441 } 2442 2443 @Override collapseBubbles()2444 public void collapseBubbles() { 2445 mMainExecutor.execute(() -> mController.collapseStack()); 2446 } 2447 2448 @Override startBubbleDrag(String bubbleKey)2449 public void startBubbleDrag(String bubbleKey) { 2450 mMainExecutor.execute(() -> mController.startBubbleDrag(bubbleKey)); 2451 } 2452 2453 @Override stopBubbleDrag(BubbleBarLocation location, int topOnScreen)2454 public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) { 2455 mMainExecutor.execute(() -> mController.stopBubbleDrag(location, topOnScreen)); 2456 } 2457 2458 @Override dragBubbleToDismiss(String key)2459 public void dragBubbleToDismiss(String key) { 2460 mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key)); 2461 } 2462 2463 @Override showUserEducation(int positionX, int positionY)2464 public void showUserEducation(int positionX, int positionY) { 2465 mMainExecutor.execute(() -> 2466 mController.showUserEducation(new Point(positionX, positionY))); 2467 } 2468 2469 @Override setBubbleBarLocation(BubbleBarLocation location)2470 public void setBubbleBarLocation(BubbleBarLocation location) { 2471 mMainExecutor.execute(() -> 2472 mController.setBubbleBarLocation(location)); 2473 } 2474 2475 @Override updateBubbleBarTopOnScreen(int topOnScreen)2476 public void updateBubbleBarTopOnScreen(int topOnScreen) { 2477 mMainExecutor.execute(() -> { 2478 mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen); 2479 if (mLayerView != null) mLayerView.updateExpandedView(); 2480 }); 2481 } 2482 } 2483 2484 private class BubblesImpl implements Bubbles { 2485 // Up-to-date cached state of bubbles data for SysUI to query from the calling thread 2486 @VisibleForTesting 2487 public class CachedState { 2488 private boolean mIsStackExpanded; 2489 private String mSelectedBubbleKey; 2490 private HashSet<String> mSuppressedBubbleKeys = new HashSet<>(); 2491 private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>(); 2492 private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>(); 2493 2494 private HashMap<String, Integer> mAppBubbleTaskIds = new HashMap(); 2495 2496 private ArrayList<Bubble> mTmpBubbles = new ArrayList<>(); 2497 2498 /** 2499 * Updates the cached state based on the last full BubbleData change. 2500 */ update(BubbleData.Update update)2501 synchronized void update(BubbleData.Update update) { 2502 if (update.selectionChanged) { 2503 mSelectedBubbleKey = update.selectedBubble != null 2504 ? update.selectedBubble.getKey() 2505 : null; 2506 } 2507 if (update.expandedChanged) { 2508 mIsStackExpanded = update.expanded; 2509 } 2510 if (update.suppressedSummaryChanged) { 2511 String summaryKey = 2512 mBubbleData.getSummaryKey(update.suppressedSummaryGroup); 2513 if (summaryKey != null) { 2514 mSuppressedGroupToNotifKeys.put(update.suppressedSummaryGroup, summaryKey); 2515 } else { 2516 mSuppressedGroupToNotifKeys.remove(update.suppressedSummaryGroup); 2517 } 2518 } 2519 2520 mTmpBubbles.clear(); 2521 mTmpBubbles.addAll(update.bubbles); 2522 mTmpBubbles.addAll(update.overflowBubbles); 2523 2524 mSuppressedBubbleKeys.clear(); 2525 mShortcutIdToBubble.clear(); 2526 mAppBubbleTaskIds.clear(); 2527 for (Bubble b : mTmpBubbles) { 2528 mShortcutIdToBubble.put(b.getShortcutId(), b); 2529 updateBubbleSuppressedState(b); 2530 2531 if (b.isAppBubble()) { 2532 mAppBubbleTaskIds.put(b.getKey(), b.getTaskId()); 2533 } 2534 } 2535 } 2536 2537 /** Sets the app bubble's taskId which is cached for SysUI. */ setAppBubbleTaskId(String key, int taskId)2538 synchronized void setAppBubbleTaskId(String key, int taskId) { 2539 mAppBubbleTaskIds.put(key, taskId); 2540 } 2541 2542 /** 2543 * Updates a specific bubble suppressed state. This is used mainly because notification 2544 * suppression changes don't go through the same BubbleData update mechanism. 2545 */ updateBubbleSuppressedState(Bubble b)2546 synchronized void updateBubbleSuppressedState(Bubble b) { 2547 if (!b.showInShade()) { 2548 mSuppressedBubbleKeys.add(b.getKey()); 2549 } else { 2550 mSuppressedBubbleKeys.remove(b.getKey()); 2551 } 2552 } 2553 isStackExpanded()2554 public synchronized boolean isStackExpanded() { 2555 return mIsStackExpanded; 2556 } 2557 isBubbleExpanded(String key)2558 public synchronized boolean isBubbleExpanded(String key) { 2559 return mIsStackExpanded && key.equals(mSelectedBubbleKey); 2560 } 2561 isBubbleNotificationSuppressedFromShade(String key, String groupKey)2562 public synchronized boolean isBubbleNotificationSuppressedFromShade(String key, 2563 String groupKey) { 2564 return mSuppressedBubbleKeys.contains(key) 2565 || (mSuppressedGroupToNotifKeys.containsKey(groupKey) 2566 && key.equals(mSuppressedGroupToNotifKeys.get(groupKey))); 2567 } 2568 2569 @Nullable getBubbleWithShortcutId(String id)2570 public synchronized Bubble getBubbleWithShortcutId(String id) { 2571 return mShortcutIdToBubble.get(id); 2572 } 2573 dump(PrintWriter pw)2574 synchronized void dump(PrintWriter pw) { 2575 pw.println("BubbleImpl.CachedState state:"); 2576 2577 pw.println("mIsStackExpanded: " + mIsStackExpanded); 2578 pw.println("mSelectedBubbleKey: " + mSelectedBubbleKey); 2579 2580 pw.println("mSuppressedBubbleKeys: " + mSuppressedBubbleKeys.size()); 2581 for (String key : mSuppressedBubbleKeys) { 2582 pw.println(" suppressing: " + key); 2583 } 2584 2585 pw.print("mSuppressedGroupToNotifKeys: "); 2586 pw.println(mSuppressedGroupToNotifKeys.size()); 2587 for (String key : mSuppressedGroupToNotifKeys.keySet()) { 2588 pw.println(" suppressing: " + key); 2589 } 2590 2591 pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values()); 2592 } 2593 } 2594 2595 private CachedState mCachedState = new CachedState(); 2596 2597 @Override isBubbleNotificationSuppressedFromShade(String key, String groupKey)2598 public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { 2599 return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey); 2600 } 2601 2602 @Override isBubbleExpanded(String key)2603 public boolean isBubbleExpanded(String key) { 2604 return mCachedState.isBubbleExpanded(key); 2605 } 2606 2607 @Override 2608 @Nullable getBubbleWithShortcutId(String shortcutId)2609 public Bubble getBubbleWithShortcutId(String shortcutId) { 2610 return mCachedState.getBubbleWithShortcutId(shortcutId); 2611 } 2612 2613 @Override collapseStack()2614 public void collapseStack() { 2615 mMainExecutor.execute(() -> { 2616 BubbleController.this.collapseStack(); 2617 }); 2618 } 2619 2620 @Override expandStackAndSelectBubble(BubbleEntry entry)2621 public void expandStackAndSelectBubble(BubbleEntry entry) { 2622 mMainExecutor.execute(() -> { 2623 BubbleController.this.expandStackAndSelectBubble(entry); 2624 }); 2625 } 2626 2627 @Override expandStackAndSelectBubble(Bubble bubble)2628 public void expandStackAndSelectBubble(Bubble bubble) { 2629 mMainExecutor.execute(() -> { 2630 BubbleController.this.expandStackAndSelectBubble(bubble); 2631 }); 2632 } 2633 2634 @Override showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon)2635 public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { 2636 mMainExecutor.execute( 2637 () -> BubbleController.this.showOrHideAppBubble(intent, user, icon)); 2638 } 2639 2640 @Override isAppBubbleTaskId(int taskId)2641 public boolean isAppBubbleTaskId(int taskId) { 2642 return mCachedState.mAppBubbleTaskIds.values().contains(taskId); 2643 } 2644 2645 @Override 2646 @Nullable getScreenshotExcludingBubble(int displayId)2647 public SynchronousScreenCaptureListener getScreenshotExcludingBubble(int displayId) { 2648 SynchronousScreenCaptureListener screenCaptureListener = 2649 ScreenCapture.createSyncCaptureListener(); 2650 2651 mMainExecutor.execute( 2652 () -> BubbleController.this.getScreenshotExcludingBubble(displayId, 2653 screenCaptureListener)); 2654 2655 return screenCaptureListener; 2656 } 2657 2658 @Override handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback, Executor callbackExecutor)2659 public boolean handleDismissalInterception(BubbleEntry entry, 2660 @Nullable List<BubbleEntry> children, IntConsumer removeCallback, 2661 Executor callbackExecutor) { 2662 IntConsumer cb = removeCallback != null 2663 ? (index) -> callbackExecutor.execute(() -> removeCallback.accept(index)) 2664 : null; 2665 return mMainExecutor.executeBlockingForResult(() -> { 2666 return BubbleController.this.handleDismissalInterception(entry, children, cb); 2667 }, Boolean.class); 2668 } 2669 2670 @Override setSysuiProxy(SysuiProxy proxy)2671 public void setSysuiProxy(SysuiProxy proxy) { 2672 mMainExecutor.execute(() -> { 2673 BubbleController.this.setSysuiProxy(proxy); 2674 }); 2675 } 2676 2677 @Override setExpandListener(BubbleExpandListener listener)2678 public void setExpandListener(BubbleExpandListener listener) { 2679 mMainExecutor.execute(() -> { 2680 BubbleController.this.setExpandListener(listener); 2681 }); 2682 } 2683 2684 @Override onEntryAdded(BubbleEntry entry)2685 public void onEntryAdded(BubbleEntry entry) { 2686 mMainExecutor.execute(() -> { 2687 BubbleController.this.onEntryAdded(entry); 2688 }); 2689 } 2690 2691 @Override onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem)2692 public void onEntryUpdated(BubbleEntry entry, boolean shouldBubbleUp, boolean fromSystem) { 2693 mMainExecutor.execute(() -> { 2694 BubbleController.this.onEntryUpdated(entry, shouldBubbleUp, fromSystem); 2695 }); 2696 } 2697 2698 @Override onEntryRemoved(BubbleEntry entry)2699 public void onEntryRemoved(BubbleEntry entry) { 2700 mMainExecutor.execute(() -> { 2701 BubbleController.this.onEntryRemoved(entry); 2702 }); 2703 } 2704 2705 @Override onRankingUpdated(RankingMap rankingMap, HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey)2706 public void onRankingUpdated(RankingMap rankingMap, 2707 HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey) { 2708 mMainExecutor.execute(() -> { 2709 BubbleController.this.onRankingUpdated(rankingMap, entryDataByKey); 2710 }); 2711 } 2712 2713 @Override onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType)2714 public void onNotificationChannelModified(String pkg, 2715 UserHandle user, NotificationChannel channel, int modificationType) { 2716 // Bubbles only cares about updates or deletions. 2717 if (modificationType == NOTIFICATION_CHANNEL_OR_GROUP_UPDATED 2718 || modificationType == NOTIFICATION_CHANNEL_OR_GROUP_DELETED) { 2719 mMainExecutor.execute(() -> { 2720 BubbleController.this.onNotificationChannelModified(pkg, user, channel, 2721 modificationType); 2722 }); 2723 } 2724 } 2725 2726 @Override onStatusBarVisibilityChanged(boolean visible)2727 public void onStatusBarVisibilityChanged(boolean visible) { 2728 mMainExecutor.execute(() -> { 2729 BubbleController.this.onStatusBarVisibilityChanged(visible); 2730 }); 2731 } 2732 2733 @Override onZenStateChanged()2734 public void onZenStateChanged() { 2735 mMainExecutor.execute(() -> { 2736 BubbleController.this.onZenStateChanged(); 2737 }); 2738 } 2739 2740 @Override onStatusBarStateChanged(boolean isShade)2741 public void onStatusBarStateChanged(boolean isShade) { 2742 mMainExecutor.execute(() -> { 2743 BubbleController.this.onStatusBarStateChanged(isShade); 2744 }); 2745 } 2746 2747 @Override onUserChanged(int newUserId)2748 public void onUserChanged(int newUserId) { 2749 mMainExecutor.execute(() -> { 2750 BubbleController.this.onUserChanged(newUserId); 2751 }); 2752 } 2753 2754 @Override onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles)2755 public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) { 2756 mMainExecutor.execute(() -> { 2757 BubbleController.this.onCurrentProfilesChanged(currentProfiles); 2758 }); 2759 } 2760 2761 @Override onUserRemoved(int removedUserId)2762 public void onUserRemoved(int removedUserId) { 2763 mMainExecutor.execute(() -> { 2764 BubbleController.this.onUserRemoved(removedUserId); 2765 }); 2766 } 2767 2768 @Override onNotificationPanelExpandedChanged(boolean expanded)2769 public void onNotificationPanelExpandedChanged(boolean expanded) { 2770 mMainExecutor.execute( 2771 () -> BubbleController.this.onNotificationPanelExpandedChanged(expanded)); 2772 } 2773 2774 @Override onSensitiveNotificationProtectionStateChanged( boolean sensitiveNotificationProtectionActive)2775 public void onSensitiveNotificationProtectionStateChanged( 2776 boolean sensitiveNotificationProtectionActive) { 2777 mMainExecutor.execute( 2778 () -> BubbleController.this.onSensitiveNotificationProtectionStateChanged( 2779 sensitiveNotificationProtectionActive)); 2780 } 2781 2782 @Override canShowBubbleNotification()2783 public boolean canShowBubbleNotification() { 2784 // in bubble bar mode, when the IME is visible we can't animate new bubbles. 2785 if (BubbleController.this.isShowingAsBubbleBar()) { 2786 return !BubbleController.this.mBubblePositioner.isImeVisible(); 2787 } 2788 return true; 2789 } 2790 } 2791 2792 /** 2793 * Bubble data that is stored per user. 2794 * Used to store and restore active bubbles during user switching. 2795 */ 2796 private static class UserBubbleData { 2797 private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>(); 2798 2799 /** 2800 * Add bubble key and whether it should be shown in notification shade 2801 */ add(String key, boolean shownInShade)2802 void add(String key, boolean shownInShade) { 2803 mKeyToShownInShadeMap.put(key, shownInShade); 2804 } 2805 2806 /** 2807 * Get all bubble keys stored for this user 2808 */ getKeys()2809 Set<String> getKeys() { 2810 return mKeyToShownInShadeMap.keySet(); 2811 } 2812 2813 /** 2814 * Check if this bubble with the given key should be shown in the notification shade 2815 */ isShownInShade(String key)2816 boolean isShownInShade(String key) { 2817 return mKeyToShownInShadeMap.get(key); 2818 } 2819 } 2820 } 2821