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.systemui.wmshell; 18 19 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; 20 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; 21 import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; 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.service.notification.NotificationStats.DISMISSAL_BUBBLE; 29 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; 30 31 import static com.android.systemui.statusbar.StatusBarState.SHADE; 32 import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; 33 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 34 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 35 36 import android.app.INotificationManager; 37 import android.app.Notification; 38 import android.app.NotificationChannel; 39 import android.app.NotificationManager; 40 import android.content.Context; 41 import android.content.pm.UserInfo; 42 import android.content.res.Configuration; 43 import android.os.RemoteException; 44 import android.os.ServiceManager; 45 import android.os.UserHandle; 46 import android.provider.Settings; 47 import android.service.notification.NotificationListenerService.RankingMap; 48 import android.service.notification.ZenModeConfig; 49 import android.util.ArraySet; 50 import android.util.Log; 51 import android.util.Pair; 52 import android.util.SparseArray; 53 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 57 import com.android.internal.annotations.VisibleForTesting; 58 import com.android.internal.statusbar.IStatusBarService; 59 import com.android.internal.statusbar.NotificationVisibility; 60 import com.android.systemui.Dumpable; 61 import com.android.systemui.dagger.SysUISingleton; 62 import com.android.systemui.dump.DumpManager; 63 import com.android.systemui.flags.FeatureFlags; 64 import com.android.systemui.model.SysUiState; 65 import com.android.systemui.plugins.statusbar.StatusBarStateController; 66 import com.android.systemui.shared.system.QuickStepContract; 67 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 68 import com.android.systemui.statusbar.NotificationShadeWindowController; 69 import com.android.systemui.statusbar.notification.NotificationChannelHelper; 70 import com.android.systemui.statusbar.notification.NotificationEntryListener; 71 import com.android.systemui.statusbar.notification.NotificationEntryManager; 72 import com.android.systemui.statusbar.notification.collection.NotifCollection; 73 import com.android.systemui.statusbar.notification.collection.NotifPipeline; 74 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 75 import com.android.systemui.statusbar.notification.collection.coordinator.BubbleCoordinator; 76 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; 77 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 78 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 79 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; 80 import com.android.systemui.statusbar.notification.logging.NotificationLogger; 81 import com.android.systemui.statusbar.phone.ShadeController; 82 import com.android.systemui.statusbar.policy.ConfigurationController; 83 import com.android.systemui.statusbar.policy.ZenModeController; 84 import com.android.wm.shell.bubbles.Bubble; 85 import com.android.wm.shell.bubbles.BubbleEntry; 86 import com.android.wm.shell.bubbles.Bubbles; 87 88 import java.io.FileDescriptor; 89 import java.io.PrintWriter; 90 import java.util.ArrayList; 91 import java.util.HashMap; 92 import java.util.List; 93 import java.util.Optional; 94 import java.util.concurrent.Executor; 95 import java.util.function.Consumer; 96 import java.util.function.IntConsumer; 97 98 /** 99 * The SysUi side bubbles manager which communicate with other SysUi components. 100 */ 101 @SysUISingleton 102 public class BubblesManager implements Dumpable { 103 104 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesManager" : TAG_BUBBLES; 105 106 private final Context mContext; 107 private final Bubbles mBubbles; 108 private final NotificationShadeWindowController mNotificationShadeWindowController; 109 private final ShadeController mShadeController; 110 private final IStatusBarService mBarService; 111 private final INotificationManager mNotificationManager; 112 private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; 113 private final NotificationGroupManagerLegacy mNotificationGroupManager; 114 private final NotificationEntryManager mNotificationEntryManager; 115 private final NotifPipeline mNotifPipeline; 116 private final Executor mSysuiMainExecutor; 117 118 private final Bubbles.SysuiProxy mSysuiProxy; 119 // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline 120 private final List<NotifCallback> mCallbacks = new ArrayList<>(); 121 122 /** 123 * Creates {@link BubblesManager}, returns {@code null} if Optional {@link Bubbles} not present 124 * which means bubbles feature not support. 125 */ 126 @Nullable create(Context context, Optional<Bubbles> bubblesOptional, NotificationShadeWindowController notificationShadeWindowController, StatusBarStateController statusBarStateController, ShadeController shadeController, ConfigurationController configurationController, @Nullable IStatusBarService statusBarService, INotificationManager notificationManager, NotificationInterruptStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManagerLegacy groupManager, NotificationEntryManager entryManager, NotifPipeline notifPipeline, SysUiState sysUiState, FeatureFlags featureFlags, DumpManager dumpManager, Executor sysuiMainExecutor)127 public static BubblesManager create(Context context, 128 Optional<Bubbles> bubblesOptional, 129 NotificationShadeWindowController notificationShadeWindowController, 130 StatusBarStateController statusBarStateController, 131 ShadeController shadeController, 132 ConfigurationController configurationController, 133 @Nullable IStatusBarService statusBarService, 134 INotificationManager notificationManager, 135 NotificationInterruptStateProvider interruptionStateProvider, 136 ZenModeController zenModeController, 137 NotificationLockscreenUserManager notifUserManager, 138 NotificationGroupManagerLegacy groupManager, 139 NotificationEntryManager entryManager, 140 NotifPipeline notifPipeline, 141 SysUiState sysUiState, 142 FeatureFlags featureFlags, 143 DumpManager dumpManager, 144 Executor sysuiMainExecutor) { 145 if (bubblesOptional.isPresent()) { 146 return new BubblesManager(context, bubblesOptional.get(), 147 notificationShadeWindowController, statusBarStateController, shadeController, 148 configurationController, statusBarService, notificationManager, 149 interruptionStateProvider, zenModeController, notifUserManager, 150 groupManager, entryManager, notifPipeline, sysUiState, featureFlags, 151 dumpManager, sysuiMainExecutor); 152 } else { 153 return null; 154 } 155 } 156 157 @VisibleForTesting BubblesManager(Context context, Bubbles bubbles, NotificationShadeWindowController notificationShadeWindowController, StatusBarStateController statusBarStateController, ShadeController shadeController, ConfigurationController configurationController, @Nullable IStatusBarService statusBarService, INotificationManager notificationManager, NotificationInterruptStateProvider interruptionStateProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, NotificationGroupManagerLegacy groupManager, NotificationEntryManager entryManager, NotifPipeline notifPipeline, SysUiState sysUiState, FeatureFlags featureFlags, DumpManager dumpManager, Executor sysuiMainExecutor)158 BubblesManager(Context context, 159 Bubbles bubbles, 160 NotificationShadeWindowController notificationShadeWindowController, 161 StatusBarStateController statusBarStateController, 162 ShadeController shadeController, 163 ConfigurationController configurationController, 164 @Nullable IStatusBarService statusBarService, 165 INotificationManager notificationManager, 166 NotificationInterruptStateProvider interruptionStateProvider, 167 ZenModeController zenModeController, 168 NotificationLockscreenUserManager notifUserManager, 169 NotificationGroupManagerLegacy groupManager, 170 NotificationEntryManager entryManager, 171 NotifPipeline notifPipeline, 172 SysUiState sysUiState, 173 FeatureFlags featureFlags, 174 DumpManager dumpManager, 175 Executor sysuiMainExecutor) { 176 mContext = context; 177 mBubbles = bubbles; 178 mNotificationShadeWindowController = notificationShadeWindowController; 179 mShadeController = shadeController; 180 mNotificationManager = notificationManager; 181 mNotificationInterruptStateProvider = interruptionStateProvider; 182 mNotificationGroupManager = groupManager; 183 mNotificationEntryManager = entryManager; 184 mNotifPipeline = notifPipeline; 185 mSysuiMainExecutor = sysuiMainExecutor; 186 187 mBarService = statusBarService == null 188 ? IStatusBarService.Stub.asInterface( 189 ServiceManager.getService(Context.STATUS_BAR_SERVICE)) 190 : statusBarService; 191 192 if (featureFlags.isNewNotifPipelineRenderingEnabled()) { 193 setupNotifPipeline(); 194 } else { 195 setupNEM(); 196 } 197 198 dumpManager.registerDumpable(TAG, this); 199 200 statusBarStateController.addCallback(new StatusBarStateController.StateListener() { 201 @Override 202 public void onStateChanged(int newState) { 203 boolean isShade = newState == SHADE; 204 bubbles.onStatusBarStateChanged(isShade); 205 } 206 }); 207 208 configurationController.addCallback(new ConfigurationController.ConfigurationListener() { 209 @Override 210 public void onConfigChanged(Configuration newConfig) { 211 mBubbles.onConfigChanged(newConfig); 212 } 213 214 @Override 215 public void onUiModeChanged() { 216 mBubbles.updateForThemeChanges(); 217 } 218 219 @Override 220 public void onThemeChanged() { 221 mBubbles.updateForThemeChanges(); 222 } 223 }); 224 225 zenModeController.addCallback(new ZenModeController.Callback() { 226 @Override 227 public void onZenChanged(int zen) { 228 mBubbles.onZenStateChanged(); 229 } 230 231 @Override 232 public void onConfigChanged(ZenModeConfig config) { 233 mBubbles.onZenStateChanged(); 234 } 235 }); 236 237 notifUserManager.addUserChangedListener( 238 new NotificationLockscreenUserManager.UserChangedListener() { 239 @Override 240 public void onUserChanged(int userId) { 241 mBubbles.onUserChanged(userId); 242 } 243 244 @Override 245 public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) { 246 mBubbles.onCurrentProfilesChanged(currentProfiles); 247 } 248 249 }); 250 251 mSysuiProxy = new Bubbles.SysuiProxy() { 252 @Override 253 public void isNotificationShadeExpand(Consumer<Boolean> callback) { 254 sysuiMainExecutor.execute(() -> { 255 callback.accept(mNotificationShadeWindowController.getPanelExpanded()); 256 }); 257 } 258 259 @Override 260 public void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback) { 261 sysuiMainExecutor.execute(() -> { 262 NotificationEntry entry = 263 mNotificationEntryManager.getPendingOrActiveNotif(key); 264 callback.accept(entry == null ? null : notifToBubbleEntry(entry)); 265 }); 266 } 267 268 @Override 269 public void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys, 270 Consumer<List<BubbleEntry>> callback) { 271 sysuiMainExecutor.execute(() -> { 272 List<BubbleEntry> result = new ArrayList<>(); 273 List<NotificationEntry> activeEntries = 274 mNotificationEntryManager.getActiveNotificationsForCurrentUser(); 275 for (int i = 0; i < activeEntries.size(); i++) { 276 NotificationEntry entry = activeEntries.get(i); 277 if (savedBubbleKeys.contains(entry.getKey()) 278 && mNotificationInterruptStateProvider.shouldBubbleUp(entry) 279 && entry.isBubble()) { 280 result.add(notifToBubbleEntry(entry)); 281 } 282 } 283 callback.accept(result); 284 }); 285 } 286 287 @Override 288 public void setNotificationInterruption(String key) { 289 sysuiMainExecutor.execute(() -> { 290 final NotificationEntry entry = 291 mNotificationEntryManager.getPendingOrActiveNotif(key); 292 if (entry != null 293 && entry.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { 294 entry.setInterruption(); 295 } 296 }); 297 } 298 299 @Override 300 public void requestNotificationShadeTopUi(boolean requestTopUi, String componentTag) { 301 sysuiMainExecutor.execute(() -> { 302 mNotificationShadeWindowController.setRequestTopUi(requestTopUi, componentTag); 303 }); 304 } 305 306 @Override 307 public void notifyRemoveNotification(String key, int reason) { 308 sysuiMainExecutor.execute(() -> { 309 final NotificationEntry entry = 310 mNotificationEntryManager.getPendingOrActiveNotif(key); 311 if (entry != null) { 312 for (NotifCallback cb : mCallbacks) { 313 cb.removeNotification(entry, getDismissedByUserStats(entry, true), 314 reason); 315 } 316 } 317 }); 318 } 319 320 @Override 321 public void notifyInvalidateNotifications(String reason) { 322 sysuiMainExecutor.execute(() -> { 323 for (NotifCallback cb : mCallbacks) { 324 cb.invalidateNotifications(reason); 325 } 326 }); 327 } 328 329 @Override 330 public void notifyMaybeCancelSummary(String key) { 331 sysuiMainExecutor.execute(() -> { 332 final NotificationEntry entry = 333 mNotificationEntryManager.getPendingOrActiveNotif(key); 334 if (entry != null) { 335 for (NotifCallback cb : mCallbacks) { 336 cb.maybeCancelSummary(entry); 337 } 338 } 339 }); 340 } 341 342 @Override 343 public void removeNotificationEntry(String key) { 344 sysuiMainExecutor.execute(() -> { 345 final NotificationEntry entry = 346 mNotificationEntryManager.getPendingOrActiveNotif(key); 347 if (entry != null) { 348 mNotificationGroupManager.onEntryRemoved(entry); 349 } 350 }); 351 } 352 353 @Override 354 public void updateNotificationBubbleButton(String key) { 355 sysuiMainExecutor.execute(() -> { 356 final NotificationEntry entry = 357 mNotificationEntryManager.getPendingOrActiveNotif(key); 358 if (entry != null && entry.getRow() != null) { 359 entry.getRow().updateBubbleButton(); 360 } 361 }); 362 } 363 364 @Override 365 public void updateNotificationSuppression(String key) { 366 sysuiMainExecutor.execute(() -> { 367 final NotificationEntry entry = 368 mNotificationEntryManager.getPendingOrActiveNotif(key); 369 if (entry != null) { 370 mNotificationGroupManager.updateSuppression(entry); 371 } 372 }); 373 } 374 375 @Override 376 public void onStackExpandChanged(boolean shouldExpand) { 377 sysuiMainExecutor.execute(() -> { 378 sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) 379 .commitUpdate(mContext.getDisplayId()); 380 if (!shouldExpand) { 381 sysUiState.setFlag( 382 QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, 383 false).commitUpdate(mContext.getDisplayId()); 384 } 385 }); 386 } 387 388 @Override 389 public void onManageMenuExpandChanged(boolean menuExpanded) { 390 sysuiMainExecutor.execute(() -> { 391 sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, 392 menuExpanded).commitUpdate(mContext.getDisplayId()); 393 }); 394 } 395 396 397 @Override 398 public void onUnbubbleConversation(String key) { 399 sysuiMainExecutor.execute(() -> { 400 final NotificationEntry entry = 401 mNotificationEntryManager.getPendingOrActiveNotif(key); 402 if (entry != null) { 403 onUserChangedBubble(entry, false /* shouldBubble */); 404 } 405 }); 406 } 407 }; 408 mBubbles.setSysuiProxy(mSysuiProxy); 409 } 410 setupNEM()411 private void setupNEM() { 412 mNotificationEntryManager.addNotificationEntryListener( 413 new NotificationEntryListener() { 414 @Override 415 public void onPendingEntryAdded(NotificationEntry entry) { 416 BubblesManager.this.onEntryAdded(entry); 417 } 418 419 @Override 420 public void onPreEntryUpdated(NotificationEntry entry) { 421 BubblesManager.this.onEntryUpdated(entry); 422 } 423 424 @Override 425 public void onEntryRemoved(NotificationEntry entry, 426 @Nullable NotificationVisibility visibility, 427 boolean removedByUser, int reason) { 428 BubblesManager.this.onEntryRemoved(entry); 429 } 430 431 @Override 432 public void onNotificationRankingUpdated(RankingMap rankingMap) { 433 BubblesManager.this.onRankingUpdate(rankingMap); 434 } 435 }); 436 437 // The new pipeline takes care of this as a NotifDismissInterceptor BubbleCoordinator 438 mNotificationEntryManager.addNotificationRemoveInterceptor( 439 (key, entry, dismissReason) -> { 440 final boolean isClearAll = dismissReason == REASON_CANCEL_ALL; 441 final boolean isUserDismiss = dismissReason == REASON_CANCEL 442 || dismissReason == REASON_CLICK; 443 final boolean isAppCancel = dismissReason == REASON_APP_CANCEL 444 || dismissReason == REASON_APP_CANCEL_ALL; 445 final boolean isSummaryCancel = 446 dismissReason == REASON_GROUP_SUMMARY_CANCELED; 447 448 // Need to check for !appCancel here because the notification may have 449 // previously been dismissed & entry.isRowDismissed would still be true 450 boolean userRemovedNotif = 451 (entry != null && entry.isRowDismissed() && !isAppCancel) 452 || isClearAll || isUserDismiss || isSummaryCancel; 453 454 if (userRemovedNotif) { 455 return handleDismissalInterception(entry); 456 } 457 return false; 458 }); 459 460 mNotificationGroupManager.registerGroupChangeListener( 461 new NotificationGroupManagerLegacy.OnGroupChangeListener() { 462 @Override 463 public void onGroupSuppressionChanged( 464 NotificationGroupManagerLegacy.NotificationGroup group, 465 boolean suppressed) { 466 // More notifications could be added causing summary to no longer 467 // be suppressed -- in this case need to remove the key. 468 final String groupKey = group.summary != null 469 ? group.summary.getSbn().getGroupKey() 470 : null; 471 if (!suppressed && groupKey != null) { 472 mBubbles.removeSuppressedSummaryIfNecessary(groupKey, null, null); 473 } 474 } 475 }); 476 477 addNotifCallback(new NotifCallback() { 478 @Override 479 public void removeNotification(NotificationEntry entry, 480 DismissedByUserStats dismissedByUserStats, int reason) { 481 mNotificationEntryManager.performRemoveNotification(entry.getSbn(), 482 dismissedByUserStats, reason); 483 } 484 485 @Override 486 public void invalidateNotifications(String reason) { 487 mNotificationEntryManager.updateNotifications(reason); 488 } 489 490 @Override 491 public void maybeCancelSummary(NotificationEntry entry) { 492 // Check if removed bubble has an associated suppressed group summary that needs 493 // to be removed now. 494 final String groupKey = entry.getSbn().getGroupKey(); 495 mBubbles.removeSuppressedSummaryIfNecessary(groupKey, (summaryKey) -> { 496 final NotificationEntry summary = 497 mNotificationEntryManager.getActiveNotificationUnfiltered(summaryKey); 498 if (summary != null) { 499 mNotificationEntryManager.performRemoveNotification( 500 summary.getSbn(), 501 getDismissedByUserStats(summary, false), 502 UNDEFINED_DISMISS_REASON); 503 } 504 }, mSysuiMainExecutor); 505 506 // Check if we still need to remove the summary from NoManGroup because the summary 507 // may not be in the mBubbleData.mSuppressedGroupKeys list and removed above. 508 // For example: 509 // 1. Bubbled notifications (group) is posted to shade and are visible bubbles 510 // 2. User expands bubbles so now their respective notifications in the shade are 511 // hidden, including the group summary 512 // 3. User removes all bubbles 513 // 4. We expect all the removed bubbles AND the summary (note: the summary was 514 // never added to the suppressedSummary list in BubbleData, so we add this check) 515 NotificationEntry summary = mNotificationGroupManager.getLogicalGroupSummary(entry); 516 if (summary != null) { 517 ArrayList<NotificationEntry> summaryChildren = 518 mNotificationGroupManager.getLogicalChildren(summary.getSbn()); 519 boolean isSummaryThisNotif = summary.getKey().equals(entry.getKey()); 520 if (!isSummaryThisNotif && (summaryChildren == null 521 || summaryChildren.isEmpty())) { 522 mNotificationEntryManager.performRemoveNotification( 523 summary.getSbn(), 524 getDismissedByUserStats(summary, false), 525 UNDEFINED_DISMISS_REASON); 526 } 527 } 528 } 529 }); 530 } 531 setupNotifPipeline()532 private void setupNotifPipeline() { 533 mNotifPipeline.addCollectionListener(new NotifCollectionListener() { 534 @Override 535 public void onEntryAdded(NotificationEntry entry) { 536 BubblesManager.this.onEntryAdded(entry); 537 } 538 539 @Override 540 public void onEntryUpdated(NotificationEntry entry) { 541 BubblesManager.this.onEntryUpdated(entry); 542 } 543 544 @Override 545 public void onEntryRemoved(NotificationEntry entry, 546 @NotifCollection.CancellationReason int reason) { 547 BubblesManager.this.onEntryRemoved(entry); 548 } 549 550 @Override 551 public void onRankingUpdate(RankingMap rankingMap) { 552 BubblesManager.this.onRankingUpdate(rankingMap); 553 } 554 }); 555 } 556 onEntryAdded(NotificationEntry entry)557 void onEntryAdded(NotificationEntry entry) { 558 if (mNotificationInterruptStateProvider.shouldBubbleUp(entry) 559 && entry.isBubble()) { 560 mBubbles.onEntryAdded(notifToBubbleEntry(entry)); 561 } 562 } 563 onEntryUpdated(NotificationEntry entry)564 void onEntryUpdated(NotificationEntry entry) { 565 mBubbles.onEntryUpdated(notifToBubbleEntry(entry), 566 mNotificationInterruptStateProvider.shouldBubbleUp(entry)); 567 } 568 onEntryRemoved(NotificationEntry entry)569 void onEntryRemoved(NotificationEntry entry) { 570 mBubbles.onEntryRemoved(notifToBubbleEntry(entry)); 571 } 572 onRankingUpdate(RankingMap rankingMap)573 void onRankingUpdate(RankingMap rankingMap) { 574 String[] orderedKeys = rankingMap.getOrderedKeys(); 575 HashMap<String, Pair<BubbleEntry, Boolean>> pendingOrActiveNotif = new HashMap<>(); 576 for (int i = 0; i < orderedKeys.length; i++) { 577 String key = orderedKeys[i]; 578 NotificationEntry entry = mNotificationEntryManager.getPendingOrActiveNotif(key); 579 BubbleEntry bubbleEntry = entry != null 580 ? notifToBubbleEntry(entry) 581 : null; 582 boolean shouldBubbleUp = entry != null 583 ? mNotificationInterruptStateProvider.shouldBubbleUp(entry) 584 : false; 585 pendingOrActiveNotif.put(key, new Pair<>(bubbleEntry, shouldBubbleUp)); 586 } 587 mBubbles.onRankingUpdated(rankingMap, pendingOrActiveNotif); 588 } 589 590 /** 591 * Gets the DismissedByUserStats used by {@link NotificationEntryManager}. 592 * Will not be necessary when using the new notification pipeline's {@link NotifCollection}. 593 * Instead, this is taken care of by {@link BubbleCoordinator}. 594 */ getDismissedByUserStats( NotificationEntry entry, boolean isVisible)595 private DismissedByUserStats getDismissedByUserStats( 596 NotificationEntry entry, 597 boolean isVisible) { 598 return new DismissedByUserStats( 599 DISMISSAL_BUBBLE, 600 DISMISS_SENTIMENT_NEUTRAL, 601 NotificationVisibility.obtain( 602 entry.getKey(), 603 entry.getRanking().getRank(), 604 mNotificationEntryManager.getActiveNotificationsCount(), 605 isVisible, 606 NotificationLogger.getNotificationLocation(entry))); 607 } 608 609 /** 610 * We intercept notification entries (including group summaries) dismissed by the user when 611 * there is an active bubble associated with it. We do this so that developers can still 612 * cancel it (and hence the bubbles associated with it). 613 * 614 * @return true if we want to intercept the dismissal of the entry, else false. 615 * @see Bubbles#handleDismissalInterception(BubbleEntry, List, IntConsumer, Executor) 616 */ handleDismissalInterception(NotificationEntry entry)617 public boolean handleDismissalInterception(NotificationEntry entry) { 618 if (entry == null) { 619 return false; 620 } 621 622 List<NotificationEntry> children = entry.getAttachedNotifChildren(); 623 List<BubbleEntry> bubbleChildren = null; 624 if (children != null) { 625 bubbleChildren = new ArrayList<>(); 626 for (int i = 0; i < children.size(); i++) { 627 bubbleChildren.add(notifToBubbleEntry(children.get(i))); 628 } 629 } 630 631 return mBubbles.handleDismissalInterception(notifToBubbleEntry(entry), bubbleChildren, 632 // TODO : b/171847985 should re-work on notification side to make this more clear. 633 (int i) -> { 634 if (i >= 0) { 635 for (NotifCallback cb : mCallbacks) { 636 cb.removeNotification(children.get(i), 637 getDismissedByUserStats(children.get(i), true), 638 REASON_GROUP_SUMMARY_CANCELED); 639 } 640 } else { 641 mNotificationGroupManager.onEntryRemoved(entry); 642 } 643 }, mSysuiMainExecutor); 644 } 645 646 /** 647 * Request the stack expand if needed, then select the specified Bubble as current. 648 * If no bubble exists for this entry, one is created. 649 * 650 * @param entry the notification for the bubble to be selected 651 */ 652 public void expandStackAndSelectBubble(NotificationEntry entry) { 653 mBubbles.expandStackAndSelectBubble(notifToBubbleEntry(entry)); 654 } 655 656 /** 657 * Request the stack expand if needed, then select the specified Bubble as current. 658 * 659 * @param bubble the bubble to be selected 660 */ 661 public void expandStackAndSelectBubble(Bubble bubble) { 662 mBubbles.expandStackAndSelectBubble(bubble); 663 } 664 665 /** 666 * @return a bubble that matches the provided shortcutId, if one exists. 667 */ 668 public Bubble getBubbleWithShortcutId(String shortcutId) { 669 return mBubbles.getBubbleWithShortcutId(shortcutId); 670 } 671 672 /** See {@link NotifCallback}. */ 673 public void addNotifCallback(NotifCallback callback) { 674 mCallbacks.add(callback); 675 } 676 677 /** 678 * When a notification is set as important, make it a bubble and expand the stack if 679 * it can bubble. 680 * 681 * @param entry the important notification. 682 */ 683 public void onUserSetImportantConversation(NotificationEntry entry) { 684 if (entry.getBubbleMetadata() == null) { 685 // No bubble metadata, nothing to do. 686 return; 687 } 688 try { 689 int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 690 mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags); 691 } catch (RemoteException e) { 692 Log.e(TAG, e.getMessage()); 693 } 694 mShadeController.collapsePanel(true); 695 if (entry.getRow() != null) { 696 entry.getRow().updateBubbleButton(); 697 } 698 } 699 700 /** 701 * Called when a user has indicated that an active notification should be shown as a bubble. 702 * <p> 703 * This method will collapse the shade, create the bubble without a flyout or dot, and suppress 704 * the notification from appearing in the shade. 705 * 706 * @param entry the notification to change bubble state for. 707 * @param shouldBubble whether the notification should show as a bubble or not. 708 */ 709 public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { 710 NotificationChannel channel = entry.getChannel(); 711 final String appPkg = entry.getSbn().getPackageName(); 712 final int appUid = entry.getSbn().getUid(); 713 if (channel == null || appPkg == null) { 714 return; 715 } 716 717 // Update the state in NotificationManagerService 718 try { 719 int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 720 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; 721 mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); 722 } catch (RemoteException e) { 723 } 724 725 // Change the settings 726 channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, 727 mNotificationManager, entry, channel); 728 channel.setAllowBubbles(shouldBubble); 729 try { 730 int currentPref = mNotificationManager.getBubblePreferenceForPackage(appPkg, appUid); 731 if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { 732 mNotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); 733 } 734 mNotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); 735 } catch (RemoteException e) { 736 Log.e(TAG, e.getMessage()); 737 } 738 739 if (shouldBubble) { 740 mShadeController.collapsePanel(true); 741 if (entry.getRow() != null) { 742 entry.getRow().updateBubbleButton(); 743 } 744 } 745 } 746 747 @Override 748 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 749 mBubbles.dump(fd, pw, args); 750 } 751 752 /** Checks whether bubbles are enabled for this user, handles negative userIds. */ 753 public static boolean areBubblesEnabled(@NonNull Context context, @NonNull UserHandle user) { 754 if (user.getIdentifier() < 0) { 755 return Settings.Secure.getInt(context.getContentResolver(), 756 NOTIFICATION_BUBBLES, 0) == 1; 757 } else { 758 return Settings.Secure.getIntForUser(context.getContentResolver(), 759 NOTIFICATION_BUBBLES, 0, user.getIdentifier()) == 1; 760 } 761 } 762 763 static BubbleEntry notifToBubbleEntry(NotificationEntry e) { 764 return new BubbleEntry(e.getSbn(), e.getRanking(), e.isClearable(), 765 e.shouldSuppressNotificationDot(), e.shouldSuppressNotificationList(), 766 e.shouldSuppressPeek()); 767 } 768 769 /** 770 * Callback for when the BubbleController wants to interact with the notification pipeline to: 771 * - Remove a previously bubbled notification 772 * - Update the notification shade since bubbled notification should/shouldn't be showing 773 */ 774 public interface NotifCallback { 775 /** 776 * Called when a bubbled notification that was hidden from the shade is now being removed 777 * This can happen when an app cancels a bubbled notification or when the user dismisses a 778 * bubble. 779 */ 780 void removeNotification(@NonNull NotificationEntry entry, 781 @NonNull DismissedByUserStats stats, int reason); 782 783 /** 784 * Called when a bubbled notification has changed whether it should be 785 * filtered from the shade. 786 */ 787 void invalidateNotifications(@NonNull String reason); 788 789 /** 790 * Called on a bubbled entry that has been removed when there are no longer 791 * bubbled entries in its group. 792 * 793 * Checks whether its group has any other (non-bubbled) children. If it doesn't, 794 * removes all remnants of the group's summary from the notification pipeline. 795 * TODO: (b/145659174) Only old pipeline needs this - delete post-migration. 796 */ 797 void maybeCancelSummary(@NonNull NotificationEntry entry); 798 } 799 } 800