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