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