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