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_GROUP_SUMMARY_CANCELED; 25 import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; 26 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; 27 28 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 29 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 30 31 import android.app.INotificationManager; 32 import android.app.Notification; 33 import android.app.NotificationChannel; 34 import android.app.NotificationManager; 35 import android.content.Context; 36 import android.content.pm.UserInfo; 37 import android.os.RemoteException; 38 import android.os.ServiceManager; 39 import android.os.UserHandle; 40 import android.provider.Settings; 41 import android.service.dreams.IDreamManager; 42 import android.service.notification.NotificationListenerService.RankingMap; 43 import android.service.notification.ZenModeConfig; 44 import android.util.Log; 45 import android.util.Pair; 46 import android.util.SparseArray; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.internal.statusbar.IStatusBarService; 53 import com.android.systemui.dagger.SysUISingleton; 54 import com.android.systemui.flags.FeatureFlags; 55 import com.android.systemui.model.SysUiState; 56 import com.android.systemui.shade.ShadeController; 57 import com.android.systemui.shared.system.QuickStepContract; 58 import com.android.systemui.statusbar.NotificationLockscreenUserManager; 59 import com.android.systemui.statusbar.NotificationShadeWindowController; 60 import com.android.systemui.statusbar.notification.NotifPipelineFlags; 61 import com.android.systemui.statusbar.notification.NotificationChannelHelper; 62 import com.android.systemui.statusbar.notification.collection.NotifCollection; 63 import com.android.systemui.statusbar.notification.collection.NotifPipeline; 64 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 65 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; 66 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 67 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 68 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; 69 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; 70 import com.android.systemui.statusbar.phone.StatusBarWindowCallback; 71 import com.android.systemui.statusbar.policy.KeyguardStateController; 72 import com.android.systemui.statusbar.policy.ZenModeController; 73 import com.android.wm.shell.bubbles.Bubble; 74 import com.android.wm.shell.bubbles.BubbleEntry; 75 import com.android.wm.shell.bubbles.Bubbles; 76 77 import java.util.ArrayList; 78 import java.util.Collection; 79 import java.util.HashMap; 80 import java.util.List; 81 import java.util.Optional; 82 import java.util.Set; 83 import java.util.concurrent.Executor; 84 import java.util.function.Consumer; 85 import java.util.function.IntConsumer; 86 87 /** 88 * The SysUi side bubbles manager which communicate with other SysUi components. 89 */ 90 @SysUISingleton 91 public class BubblesManager { 92 93 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesManager" : TAG_BUBBLES; 94 95 private final Context mContext; 96 private final Bubbles mBubbles; 97 private final NotificationShadeWindowController mNotificationShadeWindowController; 98 private final ShadeController mShadeController; 99 private final IStatusBarService mBarService; 100 private final INotificationManager mNotificationManager; 101 private final IDreamManager mDreamManager; 102 private final NotificationVisibilityProvider mVisibilityProvider; 103 private final VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider; 104 private final NotificationLockscreenUserManager mNotifUserManager; 105 private final CommonNotifCollection mCommonNotifCollection; 106 private final NotifPipeline mNotifPipeline; 107 private final NotifPipelineFlags mNotifPipelineFlags; 108 private final Executor mSysuiMainExecutor; 109 110 private final Bubbles.SysuiProxy mSysuiProxy; 111 // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline 112 private final List<NotifCallback> mCallbacks = new ArrayList<>(); 113 private final StatusBarWindowCallback mStatusBarWindowCallback; 114 115 /** 116 * Creates {@link BubblesManager}, returns {@code null} if Optional {@link Bubbles} not present 117 * which means bubbles feature not support. 118 */ 119 @Nullable create(Context context, Optional<Bubbles> bubblesOptional, NotificationShadeWindowController notificationShadeWindowController, KeyguardStateController keyguardStateController, ShadeController shadeController, @Nullable IStatusBarService statusBarService, INotificationManager notificationManager, IDreamManager dreamManager, NotificationVisibilityProvider visibilityProvider, VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, CommonNotifCollection notifCollection, NotifPipeline notifPipeline, SysUiState sysUiState, FeatureFlags featureFlags, NotifPipelineFlags notifPipelineFlags, Executor sysuiMainExecutor)120 public static BubblesManager create(Context context, 121 Optional<Bubbles> bubblesOptional, 122 NotificationShadeWindowController notificationShadeWindowController, 123 KeyguardStateController keyguardStateController, 124 ShadeController shadeController, 125 @Nullable IStatusBarService statusBarService, 126 INotificationManager notificationManager, 127 IDreamManager dreamManager, 128 NotificationVisibilityProvider visibilityProvider, 129 VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, 130 ZenModeController zenModeController, 131 NotificationLockscreenUserManager notifUserManager, 132 CommonNotifCollection notifCollection, 133 NotifPipeline notifPipeline, 134 SysUiState sysUiState, 135 FeatureFlags featureFlags, 136 NotifPipelineFlags notifPipelineFlags, 137 Executor sysuiMainExecutor) { 138 if (bubblesOptional.isPresent()) { 139 return new BubblesManager(context, 140 bubblesOptional.get(), 141 notificationShadeWindowController, 142 keyguardStateController, 143 shadeController, 144 statusBarService, 145 notificationManager, 146 dreamManager, 147 visibilityProvider, 148 visualInterruptionDecisionProvider, 149 zenModeController, 150 notifUserManager, 151 notifCollection, 152 notifPipeline, 153 sysUiState, 154 featureFlags, 155 notifPipelineFlags, 156 sysuiMainExecutor); 157 } else { 158 return null; 159 } 160 } 161 162 @VisibleForTesting BubblesManager(Context context, Bubbles bubbles, NotificationShadeWindowController notificationShadeWindowController, KeyguardStateController keyguardStateController, ShadeController shadeController, @Nullable IStatusBarService statusBarService, INotificationManager notificationManager, IDreamManager dreamManager, NotificationVisibilityProvider visibilityProvider, VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, ZenModeController zenModeController, NotificationLockscreenUserManager notifUserManager, CommonNotifCollection notifCollection, NotifPipeline notifPipeline, SysUiState sysUiState, FeatureFlags featureFlags, NotifPipelineFlags notifPipelineFlags, Executor sysuiMainExecutor)163 BubblesManager(Context context, 164 Bubbles bubbles, 165 NotificationShadeWindowController notificationShadeWindowController, 166 KeyguardStateController keyguardStateController, 167 ShadeController shadeController, 168 @Nullable IStatusBarService statusBarService, 169 INotificationManager notificationManager, 170 IDreamManager dreamManager, 171 NotificationVisibilityProvider visibilityProvider, 172 VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, 173 ZenModeController zenModeController, 174 NotificationLockscreenUserManager notifUserManager, 175 CommonNotifCollection notifCollection, 176 NotifPipeline notifPipeline, 177 SysUiState sysUiState, 178 FeatureFlags featureFlags, 179 NotifPipelineFlags notifPipelineFlags, 180 Executor sysuiMainExecutor) { 181 mContext = context; 182 mBubbles = bubbles; 183 mNotificationShadeWindowController = notificationShadeWindowController; 184 mShadeController = shadeController; 185 mNotificationManager = notificationManager; 186 mDreamManager = dreamManager; 187 mVisibilityProvider = visibilityProvider; 188 mVisualInterruptionDecisionProvider = visualInterruptionDecisionProvider; 189 mNotifUserManager = notifUserManager; 190 mCommonNotifCollection = notifCollection; 191 mNotifPipeline = notifPipeline; 192 mNotifPipelineFlags = notifPipelineFlags; 193 mSysuiMainExecutor = sysuiMainExecutor; 194 195 mBarService = statusBarService == null 196 ? IStatusBarService.Stub.asInterface( 197 ServiceManager.getService(Context.STATUS_BAR_SERVICE)) 198 : statusBarService; 199 200 setupNotifPipeline(); 201 202 keyguardStateController.addCallback(new KeyguardStateController.Callback() { 203 @Override 204 public void onKeyguardShowingChanged() { 205 boolean isUnlockedShade = !keyguardStateController.isShowing() 206 && !isDreamingOrInPreview(); 207 bubbles.onStatusBarStateChanged(isUnlockedShade); 208 } 209 }); 210 211 zenModeController.addCallback(new ZenModeController.Callback() { 212 @Override 213 public void onZenChanged(int zen) { 214 mBubbles.onZenStateChanged(); 215 } 216 217 @Override 218 public void onConfigChanged(ZenModeConfig config) { 219 mBubbles.onZenStateChanged(); 220 } 221 }); 222 223 notifUserManager.addUserChangedListener( 224 new NotificationLockscreenUserManager.UserChangedListener() { 225 @Override 226 public void onUserChanged(int userId) { 227 mBubbles.onUserChanged(userId); 228 } 229 230 @Override 231 public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) { 232 mBubbles.onCurrentProfilesChanged(currentProfiles); 233 } 234 235 @Override 236 public void onUserRemoved(int userId) { 237 mBubbles.onUserRemoved(userId); 238 } 239 240 }); 241 242 // Store callback in a field so it won't get GC'd 243 mStatusBarWindowCallback = 244 (keyguardShowing, keyguardOccluded, keyguardGoingAway, bouncerShowing, isDozing, 245 panelExpanded, isDreaming) -> 246 mBubbles.onNotificationPanelExpandedChanged(panelExpanded); 247 notificationShadeWindowController.registerCallback(mStatusBarWindowCallback); 248 249 mSysuiProxy = new Bubbles.SysuiProxy() { 250 @Override 251 public void isNotificationPanelExpand(Consumer<Boolean> callback) { 252 sysuiMainExecutor.execute(() -> { 253 callback.accept(mNotificationShadeWindowController.getPanelExpanded()); 254 }); 255 } 256 257 @Override 258 public void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback) { 259 sysuiMainExecutor.execute(() -> { 260 final NotificationEntry entry = mCommonNotifCollection.getEntry(key); 261 callback.accept(entry == null ? null : notifToBubbleEntry(entry)); 262 }); 263 } 264 265 @Override 266 public void getShouldRestoredEntries(Set<String> savedBubbleKeys, 267 Consumer<List<BubbleEntry>> callback) { 268 sysuiMainExecutor.execute(() -> { 269 List<BubbleEntry> result = new ArrayList<>(); 270 final Collection<NotificationEntry> activeEntries = 271 mCommonNotifCollection.getAllNotifs(); 272 for (NotificationEntry entry : activeEntries) { 273 if (mNotifUserManager.isCurrentProfile(entry.getSbn().getUserId()) 274 && savedBubbleKeys.contains(entry.getKey()) 275 && shouldBubbleUp(entry) 276 && entry.isBubble()) { 277 result.add(notifToBubbleEntry(entry)); 278 } 279 } 280 callback.accept(result); 281 }); 282 } 283 284 @Override 285 public void setNotificationInterruption(String key) { 286 sysuiMainExecutor.execute(() -> { 287 final NotificationEntry entry = mCommonNotifCollection.getEntry(key); 288 if (entry != null 289 && entry.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { 290 entry.setInterruption(); 291 } 292 }); 293 } 294 295 @Override 296 public void requestNotificationShadeTopUi(boolean requestTopUi, String componentTag) { 297 sysuiMainExecutor.execute(() -> { 298 mNotificationShadeWindowController.setRequestTopUi(requestTopUi, componentTag); 299 }); 300 } 301 302 @Override 303 public void notifyRemoveNotification(String key, int reason) { 304 sysuiMainExecutor.execute(() -> { 305 final NotificationEntry entry = mCommonNotifCollection.getEntry(key); 306 if (entry != null) { 307 for (NotifCallback cb : mCallbacks) { 308 cb.removeNotification(entry, getDismissedByUserStats(entry, true), 309 reason); 310 } 311 } 312 }); 313 } 314 315 @Override 316 public void notifyInvalidateNotifications(String reason) { 317 sysuiMainExecutor.execute(() -> { 318 for (NotifCallback cb : mCallbacks) { 319 cb.invalidateNotifications(reason); 320 } 321 }); 322 } 323 324 @Override 325 public void updateNotificationBubbleButton(String key) { 326 sysuiMainExecutor.execute(() -> { 327 final NotificationEntry entry = mCommonNotifCollection.getEntry(key); 328 if (entry != null && entry.getRow() != null) { 329 entry.getRow().updateBubbleButton(); 330 } 331 }); 332 } 333 334 @Override 335 public void onStackExpandChanged(boolean shouldExpand) { 336 sysuiMainExecutor.execute(() -> { 337 sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) 338 .commitUpdate(mContext.getDisplayId()); 339 if (!shouldExpand) { 340 sysUiState.setFlag( 341 QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, 342 false).commitUpdate(mContext.getDisplayId()); 343 } 344 }); 345 } 346 347 @Override 348 public void onManageMenuExpandChanged(boolean menuExpanded) { 349 sysuiMainExecutor.execute(() -> { 350 sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, 351 menuExpanded).commitUpdate(mContext.getDisplayId()); 352 }); 353 } 354 355 356 @Override 357 public void onUnbubbleConversation(String key) { 358 sysuiMainExecutor.execute(() -> { 359 final NotificationEntry entry = mCommonNotifCollection.getEntry(key); 360 if (entry != null) { 361 onUserChangedBubble(entry, false /* shouldBubble */); 362 } 363 }); 364 } 365 }; 366 mBubbles.setSysuiProxy(mSysuiProxy); 367 } 368 isDreamingOrInPreview()369 private boolean isDreamingOrInPreview() { 370 try { 371 return mDreamManager.isDreamingOrInPreview(); 372 } catch (RemoteException e) { 373 Log.e(TAG, "Failed to query dream manager.", e); 374 return false; 375 } 376 } 377 setupNotifPipeline()378 private void setupNotifPipeline() { 379 mNotifPipeline.addCollectionListener(new NotifCollectionListener() { 380 @Override 381 public void onEntryAdded(NotificationEntry entry) { 382 BubblesManager.this.onEntryAdded(entry); 383 } 384 385 @Override 386 public void onEntryUpdated(NotificationEntry entry, boolean fromSystem) { 387 BubblesManager.this.onEntryUpdated(entry, fromSystem); 388 } 389 390 @Override 391 public void onEntryRemoved(NotificationEntry entry, 392 @NotifCollection.CancellationReason int reason) { 393 if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) { 394 BubblesManager.this.onEntryRemoved(entry); 395 } 396 } 397 398 @Override 399 public void onRankingUpdate(RankingMap rankingMap) { 400 BubblesManager.this.onRankingUpdate(rankingMap); 401 } 402 403 @Override 404 public void onNotificationChannelModified( 405 String pkgName, 406 UserHandle user, 407 NotificationChannel channel, 408 int modificationType) { 409 BubblesManager.this.onNotificationChannelModified( 410 pkgName, 411 user, 412 channel, 413 modificationType); 414 } 415 }); 416 } 417 onEntryAdded(NotificationEntry entry)418 void onEntryAdded(NotificationEntry entry) { 419 if (shouldBubbleUp(entry) && entry.isBubble()) { 420 mBubbles.onEntryAdded(notifToBubbleEntry(entry)); 421 } 422 } 423 onEntryUpdated(NotificationEntry entry, boolean fromSystem)424 void onEntryUpdated(NotificationEntry entry, boolean fromSystem) { 425 mBubbles.onEntryUpdated(notifToBubbleEntry(entry), shouldBubbleUp(entry), fromSystem); 426 } 427 onEntryRemoved(NotificationEntry entry)428 void onEntryRemoved(NotificationEntry entry) { 429 mBubbles.onEntryRemoved(notifToBubbleEntry(entry)); 430 } 431 onRankingUpdate(RankingMap rankingMap)432 void onRankingUpdate(RankingMap rankingMap) { 433 String[] orderedKeys = rankingMap.getOrderedKeys(); 434 HashMap<String, Pair<BubbleEntry, Boolean>> pendingOrActiveNotif = new HashMap<>(); 435 for (int i = 0; i < orderedKeys.length; i++) { 436 String key = orderedKeys[i]; 437 final NotificationEntry entry = mCommonNotifCollection.getEntry(key); 438 BubbleEntry bubbleEntry = entry != null ? notifToBubbleEntry(entry) : null; 439 boolean shouldBubbleUp = entry != null ? shouldBubbleUp(entry) : false; 440 pendingOrActiveNotif.put(key, new Pair<>(bubbleEntry, shouldBubbleUp)); 441 } 442 mBubbles.onRankingUpdated(rankingMap, pendingOrActiveNotif); 443 } 444 onNotificationChannelModified( String pkg, UserHandle user, NotificationChannel channel, int modificationType)445 void onNotificationChannelModified( 446 String pkg, 447 UserHandle user, 448 NotificationChannel channel, 449 int modificationType) { 450 mBubbles.onNotificationChannelModified(pkg, user, channel, modificationType); 451 } 452 getDismissedByUserStats( NotificationEntry entry, boolean isVisible)453 private DismissedByUserStats getDismissedByUserStats( 454 NotificationEntry entry, 455 boolean isVisible) { 456 return new DismissedByUserStats( 457 DISMISSAL_BUBBLE, 458 DISMISS_SENTIMENT_NEUTRAL, 459 mVisibilityProvider.obtain(entry, isVisible)); 460 } 461 462 /** 463 * We intercept notification entries (including group summaries) dismissed by the user when 464 * there is an active bubble associated with it. We do this so that developers can still 465 * cancel it (and hence the bubbles associated with it). 466 * 467 * @return true if we want to intercept the dismissal of the entry, else false. 468 * @see Bubbles#handleDismissalInterception(BubbleEntry, List, IntConsumer, Executor) 469 */ handleDismissalInterception(NotificationEntry entry)470 public boolean handleDismissalInterception(NotificationEntry entry) { 471 if (entry == null) { 472 return false; 473 } 474 475 List<NotificationEntry> children = entry.getAttachedNotifChildren(); 476 List<BubbleEntry> bubbleChildren = null; 477 if (children != null) { 478 bubbleChildren = new ArrayList<>(); 479 for (int i = 0; i < children.size(); i++) { 480 bubbleChildren.add(notifToBubbleEntry(children.get(i))); 481 } 482 } 483 484 return mBubbles.handleDismissalInterception(notifToBubbleEntry(entry), bubbleChildren, 485 // TODO : b/171847985 should re-work on notification side to make this more clear. 486 (int i) -> { 487 if (i >= 0) { 488 for (NotifCallback cb : mCallbacks) { 489 cb.removeNotification(children.get(i), 490 getDismissedByUserStats(children.get(i), true), 491 REASON_GROUP_SUMMARY_CANCELED); 492 } 493 } else { 494 for (NotifCallback cb : mCallbacks) { 495 cb.removeNotification(entry, getDismissedByUserStats(entry, true), 496 REASON_GROUP_SUMMARY_CANCELED); 497 } 498 } 499 }, mSysuiMainExecutor); 500 } 501 502 /** 503 * Request the stack expand if needed, then select the specified Bubble as current. 504 * If no bubble exists for this entry, one is created. 505 * 506 * @param entry the notification for the bubble to be selected 507 */ 508 public void expandStackAndSelectBubble(NotificationEntry entry) { 509 mBubbles.expandStackAndSelectBubble(notifToBubbleEntry(entry)); 510 } 511 512 /** 513 * Request the stack expand if needed, then select the specified Bubble as current. 514 * 515 * @param bubble the bubble to be selected 516 */ 517 public void expandStackAndSelectBubble(Bubble bubble) { 518 mBubbles.expandStackAndSelectBubble(bubble); 519 } 520 521 /** 522 * @return a bubble that matches the provided shortcutId, if one exists. 523 */ 524 public Bubble getBubbleWithShortcutId(String shortcutId) { 525 return mBubbles.getBubbleWithShortcutId(shortcutId); 526 } 527 528 /** See {@link NotifCallback}. */ 529 public void addNotifCallback(NotifCallback callback) { 530 mCallbacks.add(callback); 531 } 532 533 /** 534 * When a notification is set as important, make it a bubble and expand the stack if 535 * it can bubble. 536 * 537 * @param entry the important notification. 538 */ 539 public void onUserSetImportantConversation(NotificationEntry entry) { 540 if (entry.getBubbleMetadata() == null) { 541 // No bubble metadata, nothing to do. 542 return; 543 } 544 try { 545 int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 546 mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags); 547 } catch (RemoteException e) { 548 Log.e(TAG, e.getMessage()); 549 } 550 mShadeController.collapseShade(true); 551 if (entry.getRow() != null) { 552 entry.getRow().updateBubbleButton(); 553 } 554 } 555 556 /** 557 * Called when a user has indicated that an active notification should be shown as a bubble. 558 * <p> 559 * This method will collapse the shade, create the bubble without a flyout or dot, and suppress 560 * the notification from appearing in the shade. 561 * 562 * @param entry the notification to change bubble state for. 563 * @param shouldBubble whether the notification should show as a bubble or not. 564 */ 565 public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { 566 NotificationChannel channel = entry.getChannel(); 567 final String appPkg = entry.getSbn().getPackageName(); 568 final int appUid = entry.getSbn().getUid(); 569 if (channel == null || appPkg == null) { 570 return; 571 } 572 573 entry.setFlagBubble(shouldBubble); 574 575 // Update the state in NotificationManagerService 576 try { 577 int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; 578 flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; 579 mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); 580 } catch (RemoteException e) { 581 } 582 583 // Change the settings 584 channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, 585 mNotificationManager, entry, channel); 586 channel.setAllowBubbles(shouldBubble); 587 try { 588 int currentPref = mNotificationManager.getBubblePreferenceForPackage(appPkg, appUid); 589 if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { 590 mNotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); 591 } 592 mNotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); 593 } catch (RemoteException e) { 594 Log.e(TAG, e.getMessage()); 595 } 596 597 if (shouldBubble) { 598 mShadeController.collapseShade(true); 599 if (entry.getRow() != null) { 600 entry.getRow().updateBubbleButton(); 601 } 602 } 603 } 604 605 /** Checks whether bubbles are enabled for this user, handles negative userIds. */ 606 public static boolean areBubblesEnabled(@NonNull Context context, @NonNull UserHandle user) { 607 if (user.getIdentifier() < 0) { 608 return Settings.Secure.getInt(context.getContentResolver(), 609 NOTIFICATION_BUBBLES, 0) == 1; 610 } else { 611 return Settings.Secure.getIntForUser(context.getContentResolver(), 612 NOTIFICATION_BUBBLES, 0, user.getIdentifier()) == 1; 613 } 614 } 615 616 @VisibleForTesting 617 BubbleEntry notifToBubbleEntry(NotificationEntry e) { 618 return new BubbleEntry(e.getSbn(), e.getRanking(), isDismissableFromBubbles(e), 619 e.shouldSuppressNotificationDot(), e.shouldSuppressNotificationList(), 620 e.shouldSuppressPeek()); 621 } 622 623 private boolean isDismissableFromBubbles(NotificationEntry e) { 624 // Bubbles are only accessible from the unlocked state, 625 // so we can calculate this from the Notification flags only. 626 return e.isDismissableForState(/*isLocked=*/ false); 627 } 628 629 private boolean shouldBubbleUp(NotificationEntry e) { 630 return mVisualInterruptionDecisionProvider.makeAndLogBubbleDecision(e).getShouldInterrupt(); 631 } 632 633 /** 634 * Callback for when the BubbleController wants to interact with the notification pipeline to: 635 * - Remove a previously bubbled notification 636 * - Update the notification shade since bubbled notification should/shouldn't be showing 637 */ 638 public interface NotifCallback { 639 /** 640 * Called when a bubbled notification that was hidden from the shade is now being removed 641 * This can happen when an app cancels a bubbled notification or when the user dismisses a 642 * bubble. 643 */ 644 void removeNotification(@NonNull NotificationEntry entry, 645 @NonNull DismissedByUserStats stats, int reason); 646 647 /** 648 * Called when a bubbled notification has changed whether it should be 649 * filtered from the shade. 650 */ 651 void invalidateNotifications(@NonNull String reason); 652 } 653 } 654