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