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