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