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