• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.statusbar.notification.collection;
18 
19 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
20 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
21 import static android.service.notification.NotificationListenerService.REASON_ASSISTANT_CANCEL;
22 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
23 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
24 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;
25 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_REMOVED;
26 import static android.service.notification.NotificationListenerService.REASON_CLEAR_DATA;
27 import static android.service.notification.NotificationListenerService.REASON_CLICK;
28 import static android.service.notification.NotificationListenerService.REASON_ERROR;
29 import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION;
30 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
31 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL;
32 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL_ALL;
33 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
34 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED;
35 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED;
36 import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF;
37 import static android.service.notification.NotificationListenerService.REASON_SNOOZED;
38 import static android.service.notification.NotificationListenerService.REASON_TIMEOUT;
39 import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
40 import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
41 
42 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
43 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED;
44 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
45 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
46 import static com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt.cancellationReasonDebugString;
47 
48 import static java.util.Objects.requireNonNull;
49 
50 import android.annotation.IntDef;
51 import android.annotation.MainThread;
52 import android.annotation.UserIdInt;
53 import android.app.Notification;
54 import android.app.NotificationChannel;
55 import android.os.Handler;
56 import android.os.RemoteException;
57 import android.os.Trace;
58 import android.os.UserHandle;
59 import android.service.notification.NotificationListenerService;
60 import android.service.notification.NotificationListenerService.Ranking;
61 import android.service.notification.NotificationListenerService.RankingMap;
62 import android.service.notification.StatusBarNotification;
63 import android.util.ArrayMap;
64 import android.util.Log;
65 
66 import androidx.annotation.NonNull;
67 import androidx.annotation.Nullable;
68 
69 import com.android.internal.annotations.VisibleForTesting;
70 import com.android.internal.statusbar.IStatusBarService;
71 import com.android.systemui.Dumpable;
72 import com.android.systemui.dagger.SysUISingleton;
73 import com.android.systemui.dagger.qualifiers.Background;
74 import com.android.systemui.dagger.qualifiers.Main;
75 import com.android.systemui.dump.DumpManager;
76 import com.android.systemui.dump.LogBufferEulogizer;
77 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
78 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
79 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
80 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
81 import com.android.systemui.statusbar.notification.collection.notifcollection.BindEntryEvent;
82 import com.android.systemui.statusbar.notification.collection.notifcollection.ChannelChangedEvent;
83 import com.android.systemui.statusbar.notification.collection.notifcollection.CleanUpEntryEvent;
84 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
85 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
86 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryAddedEvent;
87 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent;
88 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent;
89 import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent;
90 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
91 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionInconsistencyTracker;
92 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
93 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
94 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
95 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent;
96 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
97 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingAppliedEvent;
98 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent;
99 import com.android.systemui.statusbar.notification.collection.notifcollection.UpdateSource;
100 import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
101 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
102 import com.android.systemui.util.Assert;
103 import com.android.systemui.util.NamedListenerSet;
104 import com.android.systemui.util.time.SystemClock;
105 
106 import java.io.PrintWriter;
107 import java.lang.annotation.Retention;
108 import java.lang.annotation.RetentionPolicy;
109 import java.util.ArrayDeque;
110 import java.util.ArrayList;
111 import java.util.Collection;
112 import java.util.Collections;
113 import java.util.Comparator;
114 import java.util.HashMap;
115 import java.util.HashSet;
116 import java.util.List;
117 import java.util.Map;
118 import java.util.Objects;
119 import java.util.Queue;
120 import java.util.concurrent.Executor;
121 import java.util.concurrent.TimeUnit;
122 
123 import javax.inject.Inject;
124 
125 /**
126  * Keeps a record of all of the "active" notifications, i.e. the notifications that are currently
127  * posted to the phone. This collection is unsorted, ungrouped, and unfiltered. Just because a
128  * notification appears in this collection doesn't mean that it's currently present in the shade
129  * (notifications can be hidden for a variety of reasons). Code that cares about what notifications
130  * are *visible* right now should register listeners later in the pipeline.
131  *
132  * Each notification is represented by a {@link NotificationEntry}, which is itself made up of two
133  * parts: a {@link StatusBarNotification} and a {@link Ranking}. When notifications are updated,
134  * their underlying SBNs and Rankings are swapped out, but the enclosing NotificationEntry (and its
135  * associated key) remain the same. In general, an SBN can only be updated when the notification is
136  * reposted by the source app; Rankings are updated much more often, usually every time there is an
137  * update from any kind from NotificationManager.
138  *
139  * In general, this collection closely mirrors the list maintained by NotificationManager, but it
140  * can occasionally diverge due to lifetime extenders (see
141  * {@link #addNotificationLifetimeExtender(NotifLifetimeExtender)}).
142  *
143  * Interested parties can register listeners
144  * ({@link #addCollectionListener(NotifCollectionListener)}) to be informed when notifications
145  * events occur.
146  */
147 @MainThread
148 @SysUISingleton
149 public class NotifCollection implements Dumpable, PipelineDumpable {
150     private final IStatusBarService mStatusBarService;
151     private final SystemClock mClock;
152     private final NotifPipelineFlags mNotifPipelineFlags;
153     private final NotifCollectionLogger mLogger;
154     private final Handler mMainHandler;
155     private final Executor mBgExecutor;
156     private final LogBufferEulogizer mEulogizer;
157     private final DumpManager mDumpManager;
158     private final NotifCollectionInconsistencyTracker mInconsistencyTracker;
159     private final NotificationDismissibilityProvider mDismissibilityProvider;
160 
161     private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
162     private final Collection<NotificationEntry> mReadOnlyNotificationSet =
163             Collections.unmodifiableCollection(mNotificationSet.values());
164     private final HashMap<String, FutureDismissal> mFutureDismissals = new HashMap<>();
165 
166     @Nullable private CollectionReadyForBuildListener mBuildListener;
167     private final NamedListenerSet<NotifCollectionListener>
168             mNotifCollectionListeners = new NamedListenerSet<>();
169     private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
170     private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
171 
172 
173     private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
174     private final Runnable mRebuildListRunnable = () -> {
175         if (mBuildListener != null) {
176             mBuildListener.onBuildList(mReadOnlyNotificationSet, "asynchronousUpdate");
177         }
178     };
179 
180     private boolean mAttached = false;
181     private boolean mAmDispatchingToOtherCode;
182     private long mInitializedTimestamp = 0;
183 
184     @Inject
NotifCollection( IStatusBarService statusBarService, SystemClock clock, NotifPipelineFlags notifPipelineFlags, NotifCollectionLogger logger, @Main Handler mainHandler, @Background Executor bgExecutor, LogBufferEulogizer logBufferEulogizer, DumpManager dumpManager, NotificationDismissibilityProvider dismissibilityProvider)185     public NotifCollection(
186             IStatusBarService statusBarService,
187             SystemClock clock,
188             NotifPipelineFlags notifPipelineFlags,
189             NotifCollectionLogger logger,
190             @Main Handler mainHandler,
191             @Background Executor bgExecutor,
192             LogBufferEulogizer logBufferEulogizer,
193             DumpManager dumpManager,
194             NotificationDismissibilityProvider dismissibilityProvider) {
195         mStatusBarService = statusBarService;
196         mClock = clock;
197         mNotifPipelineFlags = notifPipelineFlags;
198         mLogger = logger;
199         mMainHandler = mainHandler;
200         mBgExecutor = bgExecutor;
201         mEulogizer = logBufferEulogizer;
202         mDumpManager = dumpManager;
203         mInconsistencyTracker = new NotifCollectionInconsistencyTracker(mLogger);
204         mDismissibilityProvider = dismissibilityProvider;
205     }
206 
207     /** Initializes the NotifCollection and registers it to receive notification events. */
attach(GroupCoalescer groupCoalescer)208     public void attach(GroupCoalescer groupCoalescer) {
209         Assert.isMainThread();
210         if (mAttached) {
211             throw new RuntimeException("attach() called twice");
212         }
213         mAttached = true;
214         mDumpManager.registerDumpable(TAG, this);
215         groupCoalescer.setNotificationHandler(mNotifHandler);
216         mInconsistencyTracker.attach(mNotificationSet::keySet, groupCoalescer::getCoalescedKeySet);
217     }
218 
219     /**
220      * Sets the class responsible for converting the collection into the list of currently-visible
221      * notifications.
222      */
setBuildListener(CollectionReadyForBuildListener buildListener)223     void setBuildListener(CollectionReadyForBuildListener buildListener) {
224         Assert.isMainThread();
225         mBuildListener = buildListener;
226     }
227 
228     /** @see NotifPipeline#getEntry(String) () */
229     @Nullable
getEntry(@onNull String key)230     public NotificationEntry getEntry(@NonNull String key) {
231         return mNotificationSet.get(key);
232     }
233 
234     /** @see NotifPipeline#getAllNotifs() */
getAllNotifs()235     Collection<NotificationEntry> getAllNotifs() {
236         Assert.isMainThread();
237         return mReadOnlyNotificationSet;
238     }
239 
240     /** @see NotifPipeline#addCollectionListener(NotifCollectionListener) */
addCollectionListener(NotifCollectionListener listener)241     void addCollectionListener(NotifCollectionListener listener) {
242         Assert.isMainThread();
243         mNotifCollectionListeners.addIfAbsent(listener);
244     }
245 
246     /** @see NotifPipeline#removeCollectionListener(NotifCollectionListener) */
removeCollectionListener(NotifCollectionListener listener)247     void removeCollectionListener(NotifCollectionListener listener) {
248         Assert.isMainThread();
249         mNotifCollectionListeners.remove(listener);
250     }
251 
252     /** @see NotifPipeline#addNotificationLifetimeExtender(NotifLifetimeExtender) */
addNotificationLifetimeExtender(NotifLifetimeExtender extender)253     void addNotificationLifetimeExtender(NotifLifetimeExtender extender) {
254         Assert.isMainThread();
255         checkForReentrantCall();
256         if (mLifetimeExtenders.contains(extender)) {
257             throw new IllegalArgumentException("Extender " + extender + " already added.");
258         }
259         mLifetimeExtenders.add(extender);
260         extender.setCallback(this::onEndLifetimeExtension);
261     }
262 
263     /** @see NotifPipeline#addNotificationDismissInterceptor(NotifDismissInterceptor) */
addNotificationDismissInterceptor(NotifDismissInterceptor interceptor)264     void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) {
265         Assert.isMainThread();
266         checkForReentrantCall();
267         if (mDismissInterceptors.contains(interceptor)) {
268             throw new IllegalArgumentException("Interceptor " + interceptor + " already added.");
269         }
270         mDismissInterceptors.add(interceptor);
271         interceptor.setCallback(this::onEndDismissInterception);
272     }
273 
274     /**
275      * Dismisses multiple notifications on behalf of the user.
276      */
dismissNotifications( List<EntryWithDismissStats> entriesToDismiss)277     public void dismissNotifications(
278             List<EntryWithDismissStats> entriesToDismiss) {
279         Assert.isMainThread();
280         checkForReentrantCall();
281 
282         entriesToDismiss = includeSummariesToDismiss(entriesToDismiss);
283 
284         final int entryCount = entriesToDismiss.size();
285         final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>();
286         for (int i = 0; i < entriesToDismiss.size(); i++) {
287             String key = entriesToDismiss.get(i).getKey();
288             int hashCode = entriesToDismiss.get(i).getEntryHashCode();
289             DismissedByUserStats stats = entriesToDismiss.get(i).getStats();
290 
291             requireNonNull(stats);
292             NotificationEntry storedEntry = mNotificationSet.get(key);
293             if (storedEntry == null) {
294                 mLogger.logDismissNonExistentNotif(key, i, entryCount);
295                 continue;
296             }
297             if (hashCode != storedEntry.hashCode()) {
298                 throw mEulogizer.record(
299                         new IllegalStateException("Invalid entry: "
300                                 + "different stored and dismissed entries for " + logKey(key)
301                                 + " (" + i + "/" + entryCount + ")"
302                                 + " dismissed=@" + Integer.toHexString(hashCode)
303                                 + " stored=@" + Integer.toHexString(storedEntry.hashCode())));
304             }
305 
306             if (storedEntry.getDismissState() == DISMISSED) {
307                 mLogger.logDismissAlreadyDismissedNotif(storedEntry, i, entryCount);
308                 continue;
309             } else if (storedEntry.getDismissState() == PARENT_DISMISSED) {
310                 mLogger.logDismissAlreadyParentDismissedNotif(storedEntry, i, entryCount);
311             }
312 
313             updateDismissInterceptors(storedEntry);
314             if (isDismissIntercepted(storedEntry)) {
315                 mLogger.logNotifDismissedIntercepted(storedEntry, i, entryCount);
316                 continue;
317             }
318 
319             entriesToLocallyDismiss.add(storedEntry);
320             if (!storedEntry.isCanceled()) {
321                 int finalI = i;
322                 // send message to system server if this notification hasn't already been cancelled
323                 mBgExecutor.execute(() -> {
324                     try {
325                         mStatusBarService.onNotificationClear(
326                                 storedEntry.getSbn().getPackageName(),
327                                 storedEntry.getSbn().getUser().getIdentifier(),
328                                 storedEntry.getSbn().getKey(),
329                                 stats.dismissalSurface,
330                                 stats.dismissalSentiment,
331                                 stats.notificationVisibility);
332                     } catch (RemoteException e) {
333                         // system process is dead if we're here.
334                         mLogger.logRemoteExceptionOnNotificationClear(
335                                 storedEntry, finalI, entryCount, e);
336                     }
337                 });
338             }
339         }
340 
341         locallyDismissNotifications(entriesToLocallyDismiss);
342         dispatchEventsAndRebuildList("dismissNotifications");
343     }
344 
includeSummariesToDismiss( List<EntryWithDismissStats> entriesToDismiss)345     private List<EntryWithDismissStats> includeSummariesToDismiss(
346             List<EntryWithDismissStats> entriesToDismiss) {
347         final HashSet<NotificationEntry> entriesSet = new HashSet<>(entriesToDismiss.size());
348         for (EntryWithDismissStats entryToStats : entriesToDismiss) {
349             NotificationEntry entry = getEntryFromDismissalStats(entryToStats);
350             if (entry != null) {
351                 entriesSet.add(entry);
352             }
353         }
354 
355         final List<EntryWithDismissStats> entriesPlusSummaries =
356                 new ArrayList<>(entriesToDismiss.size() + 1);
357         for (EntryWithDismissStats entryToStats : entriesToDismiss) {
358             entriesPlusSummaries.add(entryToStats);
359             NotificationEntry entry = getEntryFromDismissalStats(entryToStats);
360             if (entry != null) {
361                 NotificationEntry summary = fetchSummaryToDismiss(entry);
362                 if (summary != null && !entriesSet.contains(summary)) {
363                     entriesPlusSummaries.add(entryToStats.copyForEntry(summary));
364                 }
365             }
366         }
367         return entriesPlusSummaries;
368     }
369 
getEntryFromDismissalStats(EntryWithDismissStats stats)370     private NotificationEntry getEntryFromDismissalStats(EntryWithDismissStats stats) {
371         if (NotificationBundleUi.isEnabled()) {
372            return mNotificationSet.get(stats.getKey());
373         } else {
374             return stats.getEntry();
375         }
376     }
377 
378     /**
379      * Dismisses a single notification on behalf of the user.
380      */
dismissNotification( NotificationEntry entry, @NonNull DismissedByUserStats stats)381     public void dismissNotification(
382             NotificationEntry entry,
383             @NonNull DismissedByUserStats stats) {
384         dismissNotifications(List.of(new EntryWithDismissStats(
385                 entry, stats, entry.getKey(), entry.hashCode())));
386     }
387 
388     /**
389      * Dismisses all clearable notifications for a given userid on behalf of the user.
390      */
dismissAllNotifications(@serIdInt int userId)391     public void dismissAllNotifications(@UserIdInt int userId) {
392         Assert.isMainThread();
393         checkForReentrantCall();
394 
395         mLogger.logDismissAll(userId);
396 
397         try {
398             // TODO(b/169585328): Do not clear media player notifications
399             mStatusBarService.onClearAllNotifications(userId);
400         } catch (RemoteException e) {
401             // system process is dead if we're here.
402             mLogger.logRemoteExceptionOnClearAllNotifications(e);
403         }
404 
405         final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
406         final int initialEntryCount = entries.size();
407         for (int i = entries.size() - 1; i >= 0; i--) {
408             NotificationEntry entry = entries.get(i);
409 
410             if (!shouldDismissOnClearAll(entry, userId)) {
411                 // system server won't be removing these notifications, but we still give dismiss
412                 // interceptors the chance to filter the notification
413                 updateDismissInterceptors(entry);
414                 if (isDismissIntercepted(entry)) {
415                     mLogger.logNotifClearAllDismissalIntercepted(entry, i, initialEntryCount);
416                 }
417                 entries.remove(i);
418             }
419         }
420 
421         locallyDismissNotifications(entries);
422         dispatchEventsAndRebuildList("dismissAllNotifications");
423     }
424 
425     /**
426      * Optimistically marks the given notifications as dismissed -- we'll wait for the signal
427      * from system server before removing it from our notification set.
428      */
locallyDismissNotifications(List<NotificationEntry> entries)429     private void locallyDismissNotifications(List<NotificationEntry> entries) {
430         final List<NotificationEntry> canceledEntries = new ArrayList<>();
431         final int entryCount = entries.size();
432         for (int i = 0; i < entries.size(); i++) {
433             NotificationEntry entry = entries.get(i);
434 
435             final NotificationEntry storedEntry = mNotificationSet.get(entry.getKey());
436             if (storedEntry == null) {
437                 mLogger.logLocallyDismissNonExistentNotif(entry, i, entryCount);
438             } else if (storedEntry != entry) {
439                 mLogger.logLocallyDismissMismatchedEntry(entry, i, entryCount, storedEntry);
440             }
441 
442             if (entry.getDismissState() == DISMISSED) {
443                 mLogger.logLocallyDismissAlreadyDismissedNotif(entry, i, entryCount);
444             } else if (entry.getDismissState() == PARENT_DISMISSED) {
445                 mLogger.logLocallyDismissAlreadyParentDismissedNotif(entry, i, entryCount);
446             }
447 
448             entry.setDismissState(DISMISSED);
449             mLogger.logLocallyDismissed(entry, i, entryCount);
450 
451             if (entry.isCanceled()) {
452                 canceledEntries.add(entry);
453                 continue;
454             }
455 
456             // Mark any children as dismissed as system server will auto-dismiss them as well
457             if (entry.getSbn().getNotification().isGroupSummary()) {
458                 for (NotificationEntry otherEntry : mNotificationSet.values()) {
459                     if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) {
460                         if (otherEntry.getDismissState() == DISMISSED) {
461                             mLogger.logLocallyDismissAlreadyDismissedChild(
462                                     otherEntry, entry, i, entryCount);
463                         } else if (otherEntry.getDismissState() == PARENT_DISMISSED) {
464                             mLogger.logLocallyDismissAlreadyParentDismissedChild(
465                                     otherEntry, entry, i, entryCount);
466                         }
467                         otherEntry.setDismissState(PARENT_DISMISSED);
468                         mLogger.logLocallyDismissedChild(otherEntry, entry, i, entryCount);
469                         if (otherEntry.isCanceled()) {
470                             canceledEntries.add(otherEntry);
471                         }
472                     }
473                 }
474             }
475         }
476 
477         // Immediately remove any dismissed notifs that have already been canceled by system server
478         // (probably due to being lifetime-extended up until this point).
479         for (NotificationEntry canceledEntry : canceledEntries) {
480             mLogger.logLocallyDismissedAlreadyCanceledEntry(canceledEntry);
481             tryRemoveNotification(canceledEntry);
482         }
483     }
484 
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)485     private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
486         Assert.isMainThread();
487 
488         postNotification(sbn, requireRanking(rankingMap, sbn.getKey()));
489         applyRanking(rankingMap);
490         dispatchEventsAndRebuildList("onNotificationPosted");
491     }
492 
onNotificationGroupPosted(List<CoalescedEvent> batch)493     private void onNotificationGroupPosted(List<CoalescedEvent> batch) {
494         Assert.isMainThread();
495 
496         mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size());
497 
498         for (CoalescedEvent event : batch) {
499             postNotification(event.getSbn(), event.getRanking());
500         }
501         dispatchEventsAndRebuildList("onNotificationGroupPosted");
502     }
503 
onNotificationRemoved( StatusBarNotification sbn, RankingMap rankingMap, int reason)504     private void onNotificationRemoved(
505             StatusBarNotification sbn,
506             RankingMap rankingMap,
507             int reason) {
508         Assert.isMainThread();
509 
510         mLogger.logNotifRemoved(sbn, reason);
511 
512         final NotificationEntry entry = mNotificationSet.get(sbn.getKey());
513         if (entry == null) {
514             // TODO (b/160008901): Throw an exception here
515             mLogger.logNoNotificationToRemoveWithKey(sbn, reason);
516             return;
517         }
518 
519         entry.mCancellationReason = reason;
520         tryRemoveNotification(entry);
521         applyRanking(rankingMap);
522         dispatchEventsAndRebuildList("onNotificationRemoved");
523     }
524 
onNotificationRankingUpdate(RankingMap rankingMap)525     private void onNotificationRankingUpdate(RankingMap rankingMap) {
526         Assert.isMainThread();
527         mEventQueue.add(new RankingUpdatedEvent(rankingMap));
528         applyRanking(rankingMap);
529         dispatchEventsAndRebuildList("onNotificationRankingUpdate");
530     }
531 
onNotificationChannelModified( String pkgName, UserHandle user, NotificationChannel channel, int modificationType)532     private void onNotificationChannelModified(
533             String pkgName,
534             UserHandle user,
535             NotificationChannel channel,
536             int modificationType) {
537         Assert.isMainThread();
538         mEventQueue.add(new ChannelChangedEvent(pkgName, user, channel, modificationType));
539         dispatchEventsAndAsynchronouslyRebuildList();
540     }
541 
onNotificationsInitialized()542     private void onNotificationsInitialized() {
543         mInitializedTimestamp = UseElapsedRealtimeForCreationTime.getCurrentTime(mClock);
544     }
545 
postNotification( StatusBarNotification sbn, Ranking ranking)546     private void postNotification(
547             StatusBarNotification sbn,
548             Ranking ranking) {
549         NotificationEntry entry = mNotificationSet.get(sbn.getKey());
550 
551         if (entry == null) {
552             // A new notification!
553             entry = new NotificationEntry(sbn, ranking,
554                     UseElapsedRealtimeForCreationTime.getCurrentTime(mClock));
555             mEventQueue.add(new InitEntryEvent(entry));
556             mEventQueue.add(new BindEntryEvent(entry, sbn));
557             mNotificationSet.put(sbn.getKey(), entry);
558 
559             mLogger.logNotifPosted(entry);
560             mEventQueue.add(new EntryAddedEvent(entry));
561 
562         } else {
563             // Update to an existing entry
564 
565             // Notification is updated so it is essentially re-added and thus alive again, so we
566             // can reset its state.
567             // TODO: If a coalesced event ever gets here, it's possible to lose track of children,
568             //  since their rankings might have been updated earlier (and thus we may no longer
569             //  think a child is associated with this locally-dismissed entry).
570             // If the postTime remains the same, we can assume the update is from SystemServer, not
571             // the app.
572             long lastUpdateTime = entry.getSbn().getPostTime();
573             UpdateSource source = sbn.getPostTime() == lastUpdateTime
574                     ? UpdateSource.SystemServer
575                     : UpdateSource.App;
576             cancelLocalDismissal(entry);
577             cancelLifetimeExtension(entry);
578             cancelDismissInterception(entry);
579             entry.mCancellationReason = REASON_NOT_CANCELED;
580 
581             entry.setSbn(sbn);
582             mEventQueue.add(new BindEntryEvent(entry, sbn));
583 
584             mLogger.logNotifUpdated(entry);
585             mEventQueue.add(new EntryUpdatedEvent(entry, source));
586         }
587     }
588 
589     /**
590      * Tries to remove a notification from the notification set. This removal may be blocked by
591      * lifetime extenders. Does not trigger a rebuild of the list; caller must do that manually.
592      *
593      * @return True if the notification was removed, false otherwise.
594      */
tryRemoveNotification(NotificationEntry entry)595     private boolean tryRemoveNotification(NotificationEntry entry) {
596         final NotificationEntry storedEntry = mNotificationSet.get(entry.getKey());
597         if (storedEntry == null) {
598             Log.wtf(TAG, "TRY REMOVE non-existent notification " + logKey(entry));
599             return false;
600         } else if (storedEntry != entry) {
601             throw mEulogizer.record(
602                     new IllegalStateException("Mismatched stored and tryRemoved entries"
603                             + " for key " + logKey(entry) + ":"
604                             + " stored=@" + Integer.toHexString(storedEntry.hashCode())
605                             + " tryRemoved=@" + Integer.toHexString(entry.hashCode())));
606         }
607 
608         if (!entry.isCanceled()) {
609             throw mEulogizer.record(
610                     new IllegalStateException("Cannot remove notification " + logKey(entry)
611                             + ": has not been marked for removal"));
612         }
613 
614         if (cannotBeLifetimeExtended(entry)) {
615             cancelLifetimeExtension(entry);
616         } else {
617             updateLifetimeExtension(entry);
618         }
619 
620         if (!isLifetimeExtended(entry)) {
621             mLogger.logNotifReleased(entry);
622             mNotificationSet.remove(entry.getKey());
623             cancelDismissInterception(entry);
624             mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason));
625             mEventQueue.add(new CleanUpEntryEvent(entry));
626             handleFutureDismissal(entry);
627             return true;
628         } else {
629             return false;
630         }
631     }
632 
633     /**
634      * Get the group summary entry
635      * @param groupKey
636      * @return
637      */
638     @Nullable
getGroupSummary(String groupKey)639     public NotificationEntry getGroupSummary(String groupKey) {
640         return mNotificationSet
641                 .values()
642                 .stream()
643                 .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey))
644                 .filter(it -> it.getSbn().getNotification().isGroupSummary())
645                 .findFirst().orElse(null);
646     }
647 
isDismissable(NotificationEntry entry)648     private boolean isDismissable(NotificationEntry entry) {
649         return mDismissibilityProvider.isDismissable(entry.getKey());
650     }
651 
652     /**
653      * Checks if the entry is the only child in the logical group;
654      * it need not have a summary to qualify
655      *
656      * @param entry the entry to check
657      */
isOnlyChildInGroup(NotificationEntry entry)658     public boolean isOnlyChildInGroup(NotificationEntry entry) {
659         String groupKey = entry.getSbn().getGroupKey();
660         return mNotificationSet.get(entry.getKey()) == entry
661                 && mNotificationSet
662                 .values()
663                 .stream()
664                 .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey))
665                 .filter(it -> !it.getSbn().getNotification().isGroupSummary())
666                 .count() == 1;
667     }
668 
applyRanking(@onNull RankingMap rankingMap)669     private void applyRanking(@NonNull RankingMap rankingMap) {
670         ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null;
671         for (NotificationEntry entry : mNotificationSet.values()) {
672             if (!entry.isCanceled()) {
673 
674                 // TODO: (b/148791039) We should crash if we are ever handed a ranking with
675                 //  incomplete entries. Right now, there's a race condition in NotificationListener
676                 //  that means this might occur when SystemUI is starting up.
677                 Ranking ranking = new Ranking();
678                 if (rankingMap.getRanking(entry.getKey(), ranking)) {
679                     entry.setRanking(ranking);
680 
681                     // TODO: (b/145659174) update the sbn's overrideGroupKey in
682                     //  NotificationEntry.setRanking instead of here once we fully migrate to the
683                     //  NewNotifPipeline
684                     final String newOverrideGroupKey = ranking.getOverrideGroupKey();
685                     if (!Objects.equals(entry.getSbn().getOverrideGroupKey(),
686                             newOverrideGroupKey)) {
687                         entry.getSbn().setOverrideGroupKey(newOverrideGroupKey);
688                     }
689                 } else {
690                     if (currentEntriesWithoutRankings == null) {
691                         currentEntriesWithoutRankings = new ArrayMap<>();
692                     }
693                     currentEntriesWithoutRankings.put(entry.getKey(), entry);
694                 }
695             }
696         }
697 
698         mInconsistencyTracker.logNewMissingNotifications(rankingMap);
699         mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap);
700         if (currentEntriesWithoutRankings != null) {
701             for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
702                 entry.mCancellationReason = REASON_UNKNOWN;
703                 tryRemoveNotification(entry);
704             }
705         }
706         mEventQueue.add(new RankingAppliedEvent());
707     }
708 
dispatchEventsAndRebuildList(String reason)709     private void dispatchEventsAndRebuildList(String reason) {
710         Trace.beginSection("NotifCollection.dispatchEventsAndRebuildList");
711         if (mMainHandler.hasCallbacks(mRebuildListRunnable)) {
712             mMainHandler.removeCallbacks(mRebuildListRunnable);
713         }
714 
715         dispatchEvents();
716 
717         if (mBuildListener != null) {
718             mBuildListener.onBuildList(mReadOnlyNotificationSet, reason);
719         }
720         Trace.endSection();
721     }
722 
dispatchEventsAndAsynchronouslyRebuildList()723     private void dispatchEventsAndAsynchronouslyRebuildList() {
724         Trace.beginSection("NotifCollection.dispatchEventsAndAsynchronouslyRebuildList");
725 
726         dispatchEvents();
727 
728         if (!mMainHandler.hasCallbacks(mRebuildListRunnable)) {
729             mMainHandler.postDelayed(mRebuildListRunnable, 1000L);
730         }
731 
732         Trace.endSection();
733     }
734 
dispatchEvents()735     private void dispatchEvents() {
736         Trace.beginSection("NotifCollection.dispatchEvents");
737 
738         mAmDispatchingToOtherCode = true;
739         while (!mEventQueue.isEmpty()) {
740             mEventQueue.remove().dispatchTo(mNotifCollectionListeners);
741         }
742         mAmDispatchingToOtherCode = false;
743 
744         Trace.endSection();
745     }
746 
onEndLifetimeExtension( @onNull NotifLifetimeExtender extender, @NonNull NotificationEntry entry)747     private void onEndLifetimeExtension(
748             @NonNull NotifLifetimeExtender extender,
749             @NonNull NotificationEntry entry) {
750         Assert.isMainThread();
751         if (!mAttached) {
752             return;
753         }
754         checkForReentrantCall();
755 
756         NotificationEntry collectionEntry = getEntry(entry.getKey());
757         String logKey = logKey(entry);
758         String collectionEntryIs = collectionEntry == null ? "null"
759                 : entry == collectionEntry ? "same" : "different";
760 
761         if (entry != collectionEntry) {
762             // TODO: We should probably make this throw, but that's too risky right now
763             mLogger.logEntryBeingExtendedNotInCollection(entry, extender, collectionEntryIs);
764         }
765 
766         if (!entry.mLifetimeExtenders.remove(extender)) {
767             throw mEulogizer.record(new IllegalStateException(
768                     String.format("Cannot end lifetime extension for extender \"%s\""
769                                     + " of entry %s (collection entry is %s)",
770                             extender.getName(), logKey, collectionEntryIs)));
771         }
772 
773         mLogger.logLifetimeExtensionEnded(entry, extender, entry.mLifetimeExtenders.size());
774 
775         if (!isLifetimeExtended(entry)) {
776             if (tryRemoveNotification(entry)) {
777                 dispatchEventsAndRebuildList("onEndLifetimeExtension");
778             }
779         }
780     }
781 
cancelLifetimeExtension(NotificationEntry entry)782     private void cancelLifetimeExtension(NotificationEntry entry) {
783         mAmDispatchingToOtherCode = true;
784         for (NotifLifetimeExtender extender : entry.mLifetimeExtenders) {
785             extender.cancelLifetimeExtension(entry);
786         }
787         mAmDispatchingToOtherCode = false;
788         entry.mLifetimeExtenders.clear();
789     }
790 
isLifetimeExtended(NotificationEntry entry)791     private boolean isLifetimeExtended(NotificationEntry entry) {
792         return entry.mLifetimeExtenders.size() > 0;
793     }
794 
updateLifetimeExtension(NotificationEntry entry)795     private void updateLifetimeExtension(NotificationEntry entry) {
796         entry.mLifetimeExtenders.clear();
797         mAmDispatchingToOtherCode = true;
798         for (NotifLifetimeExtender extender : mLifetimeExtenders) {
799             if (extender.maybeExtendLifetime(entry, entry.mCancellationReason)) {
800                 mLogger.logLifetimeExtended(entry, extender);
801                 entry.mLifetimeExtenders.add(extender);
802             }
803         }
804         mAmDispatchingToOtherCode = false;
805     }
806 
updateDismissInterceptors(@onNull NotificationEntry entry)807     private void updateDismissInterceptors(@NonNull NotificationEntry entry) {
808         entry.mDismissInterceptors.clear();
809         mAmDispatchingToOtherCode = true;
810         for (NotifDismissInterceptor interceptor : mDismissInterceptors) {
811             if (interceptor.shouldInterceptDismissal(entry)) {
812                 entry.mDismissInterceptors.add(interceptor);
813             }
814         }
815         mAmDispatchingToOtherCode = false;
816     }
817 
cancelLocalDismissal(NotificationEntry entry)818     private void cancelLocalDismissal(NotificationEntry entry) {
819         if (entry.getDismissState() == NOT_DISMISSED) {
820             mLogger.logCancelLocalDismissalNotDismissedNotif(entry);
821             return;
822         }
823         entry.setDismissState(NOT_DISMISSED);
824         if (entry.getSbn().getNotification().isGroupSummary()) {
825             for (NotificationEntry otherEntry : mNotificationSet.values()) {
826                 if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey())
827                         && otherEntry.getDismissState() == PARENT_DISMISSED) {
828                     otherEntry.setDismissState(NOT_DISMISSED);
829                 }
830             }
831         }
832     }
833 
onEndDismissInterception( NotifDismissInterceptor interceptor, NotificationEntry entry, @NonNull DismissedByUserStats stats)834     private void onEndDismissInterception(
835             NotifDismissInterceptor interceptor,
836             NotificationEntry entry,
837             @NonNull DismissedByUserStats stats) {
838         Assert.isMainThread();
839         if (!mAttached) {
840             return;
841         }
842         checkForReentrantCall();
843 
844         if (!entry.mDismissInterceptors.remove(interceptor)) {
845             throw mEulogizer.record(new IllegalStateException(
846                     String.format(
847                             "Cannot end dismiss interceptor for interceptor \"%s\" (%s)",
848                             interceptor.getName(),
849                             interceptor)));
850         }
851 
852         if (!isDismissIntercepted(entry)) {
853             dismissNotification(entry, stats);
854         }
855     }
856 
cancelDismissInterception(NotificationEntry entry)857     private void cancelDismissInterception(NotificationEntry entry) {
858         mAmDispatchingToOtherCode = true;
859         for (NotifDismissInterceptor interceptor : entry.mDismissInterceptors) {
860             interceptor.cancelDismissInterception(entry);
861         }
862         mAmDispatchingToOtherCode = false;
863         entry.mDismissInterceptors.clear();
864     }
865 
isDismissIntercepted(NotificationEntry entry)866     private boolean isDismissIntercepted(NotificationEntry entry) {
867         return entry.mDismissInterceptors.size() > 0;
868     }
869 
checkForReentrantCall()870     private void checkForReentrantCall() {
871         if (mAmDispatchingToOtherCode) {
872             throw mEulogizer.record(new IllegalStateException("Reentrant call detected"));
873         }
874     }
875 
876     // While the NotificationListener is connecting to NotificationManager, there is a short period
877     // during which it's possible for us to receive events about notifications we don't yet know
878     // about (or that otherwise don't make sense). Until that race condition is fixed, we create a
879     // "forgiveness window" of five seconds during which we won't crash if we receive nonsensical
880     // messages from system server.
crashIfNotInitializing(RuntimeException exception)881     private void crashIfNotInitializing(RuntimeException exception) {
882         final boolean isRecentlyInitialized = mInitializedTimestamp == 0
883                 || UseElapsedRealtimeForCreationTime.getCurrentTime(mClock) - mInitializedTimestamp
884                         < INITIALIZATION_FORGIVENESS_WINDOW;
885 
886         if (isRecentlyInitialized) {
887             mLogger.logIgnoredError(exception.getMessage());
888         } else {
889             throw mEulogizer.record(exception);
890         }
891     }
892 
893     private static Ranking requireRanking(RankingMap rankingMap, String key) {
894         // TODO: Modify RankingMap so that we don't have to make a copy here
895         Ranking ranking = new Ranking();
896         if (!rankingMap.getRanking(key, ranking)) {
897             throw new IllegalArgumentException("Ranking map doesn't contain key: " + key);
898         }
899         return ranking;
900     }
901 
902     private boolean cannotBeLifetimeExtended(NotificationEntry entry) {
903         final boolean locallyDismissedByUser = entry.getDismissState() != NOT_DISMISSED;
904         final boolean systemServerReportedUserCancel =
905                 entry.mCancellationReason == REASON_CLICK
906                         || entry.mCancellationReason == REASON_CANCEL;
907         return locallyDismissedByUser || systemServerReportedUserCancel;
908     }
909 
910     /**
911      * When a group summary is dismissed, NotificationManager will also try to dismiss its children.
912      * Returns true if we think dismissing the group summary with group key
913      * <code>dismissedGroupKey</code> will cause NotificationManager to also dismiss
914      * <code>entry</code>.
915      *
916      * See NotificationManager.cancelGroupChildrenByListLocked() for corresponding code.
917      */
918     @VisibleForTesting
919     static boolean shouldAutoDismissChildren(
920             NotificationEntry entry,
921             String dismissedGroupKey) {
922         return entry.getSbn().getGroupKey().equals(dismissedGroupKey)
923                 && !entry.getSbn().getNotification().isGroupSummary()
924                 && !hasFlag(entry, Notification.FLAG_ONGOING_EVENT)
925                 && !hasFlag(entry, Notification.FLAG_BUBBLE)
926                 && !hasFlag(entry, Notification.FLAG_NO_CLEAR)
927                 && (entry.getChannel() == null || !entry.getChannel().isImportantConversation())
928                 && entry.getDismissState() != DISMISSED;
929     }
930 
931     /**
932      * When the user 'clears all notifications' through SystemUI, NotificationManager will not
933      * dismiss unclearable notifications.
934      * @return true if we think NotificationManager will dismiss the entry when asked to
935      * cancel this notification with {@link NotificationListenerService#REASON_CANCEL_ALL}
936      *
937      * See NotificationManager.cancelAllLocked for corresponding code.
938      */
939     private static boolean shouldDismissOnClearAll(
940             NotificationEntry entry,
941             @UserIdInt int userId) {
942         return userIdMatches(entry, userId)
943                 && entry.isClearable()
944                 && !hasFlag(entry, Notification.FLAG_BUBBLE)
945                 && entry.getDismissState() != DISMISSED;
946     }
947 
948     private static boolean hasFlag(NotificationEntry entry, int flag) {
949         return (entry.getSbn().getNotification().flags & flag) != 0;
950     }
951 
952     /**
953      * Determine whether the userId applies to the notification in question, either because
954      * they match exactly, or one of them is USER_ALL (which is treated as a wildcard).
955      *
956      * See NotificationManager#notificationMatchesUserId
957      */
958     private static boolean userIdMatches(NotificationEntry entry, int userId) {
959         return userId == UserHandle.USER_ALL
960                 || entry.getSbn().getUser().getIdentifier() == UserHandle.USER_ALL
961                 || entry.getSbn().getUser().getIdentifier() == userId;
962     }
963 
964     @Override
965     public void dump(PrintWriter pw, @NonNull String[] args) {
966         final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
967         entries.sort(Comparator.comparing(NotificationEntry::getKey));
968 
969         pw.println("\t" + TAG + " unsorted/unfiltered notifications: " + entries.size());
970         pw.println(
971                 ListDumper.dumpList(
972                         entries,
973                         true,
974                         "\t\t"));
975 
976         mInconsistencyTracker.dump(pw);
977     }
978 
979     @Override
980     public void dumpPipeline(@NonNull PipelineDumper d) {
981         d.dump("notifCollectionListeners", mNotifCollectionListeners);
982         d.dump("lifetimeExtenders", mLifetimeExtenders);
983         d.dump("dismissInterceptors", mDismissInterceptors);
984         d.dump("buildListener", mBuildListener);
985     }
986 
987     private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() {
988         @Override
989         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
990             NotifCollection.this.onNotificationPosted(sbn, rankingMap);
991         }
992 
993         @Override
994         public void onNotificationBatchPosted(List<CoalescedEvent> events) {
995             NotifCollection.this.onNotificationGroupPosted(events);
996         }
997 
998         @Override
999         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
1000             NotifCollection.this.onNotificationRemoved(sbn, rankingMap, REASON_UNKNOWN);
1001         }
1002 
1003         @Override
1004         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
1005                 int reason) {
1006             NotifCollection.this.onNotificationRemoved(sbn, rankingMap, reason);
1007         }
1008 
1009         @Override
1010         public void onNotificationRankingUpdate(RankingMap rankingMap) {
1011             NotifCollection.this.onNotificationRankingUpdate(rankingMap);
1012         }
1013 
1014         @Override
1015         public void onNotificationChannelModified(
1016                 String pkgName,
1017                 UserHandle user,
1018                 NotificationChannel channel,
1019                 int modificationType) {
1020             NotifCollection.this.onNotificationChannelModified(
1021                     pkgName,
1022                     user,
1023                     channel,
1024                     modificationType);
1025         }
1026 
1027         @Override
1028         public void onNotificationsInitialized() {
1029             NotifCollection.this.onNotificationsInitialized();
1030         }
1031     };
1032 
1033     private static final String TAG = "NotifCollection";
1034 
1035     /**
1036      * Get an object which can be used to update a notification (internally to the pipeline)
1037      * in response to a user action.
1038      *
1039      * @param name the name of the component that will update notifiations
1040      * @return an updater
1041      */
1042     public InternalNotifUpdater getInternalNotifUpdater(String name) {
1043         return (sbn, reason) -> mMainHandler.post(
1044                 () -> updateNotificationInternally(sbn, name, reason));
1045     }
1046 
1047     /**
1048      * Provide an updated StatusBarNotification for an existing entry.  If no entry exists for the
1049      * given notification key, this method does nothing.
1050      *
1051      * @param sbn the updated notification
1052      * @param name the component which is updating the notification
1053      * @param reason the reason the notification is being updated
1054      */
updateNotificationInternally(StatusBarNotification sbn, String name, String reason)1055     private void updateNotificationInternally(StatusBarNotification sbn, String name,
1056             String reason) {
1057         Assert.isMainThread();
1058         checkForReentrantCall();
1059 
1060         // Make sure we have the notification to update
1061         NotificationEntry entry = mNotificationSet.get(sbn.getKey());
1062         if (entry == null) {
1063             mLogger.logNotifInternalUpdateFailed(sbn, name, reason);
1064             return;
1065         }
1066         mLogger.logNotifInternalUpdate(entry, name, reason);
1067 
1068         // First do the pieces of postNotification which are not about assuming the notification
1069         // was sent by the app
1070         entry.setSbn(sbn);
1071         mEventQueue.add(new BindEntryEvent(entry, sbn));
1072 
1073         mLogger.logNotifUpdated(entry);
1074         mEventQueue.add(new EntryUpdatedEvent(entry, UpdateSource.SystemUi));
1075 
1076         // Skip the applyRanking step and go straight to dispatching the events
1077         dispatchEventsAndRebuildList("updateNotificationInternally");
1078     }
1079 
1080     /**
1081      * A method to alert the collection that an async operation is happening, at the end of which a
1082      * dismissal request will be made.  This method has the additional guarantee that if a parent
1083      * notification exists for a single child, then that notification will also be dismissed.
1084      *
1085      * The runnable returned must be run at the end of the async operation to enact the cancellation
1086      *
1087      * @param entry the notification we want to dismiss
1088      * @param cancellationReason the reason for the cancellation
1089      * @param statsCreator the callback for generating the stats for an entry
1090      * @return the runnable to be run when the dismissal is ready to happen
1091      */
registerFutureDismissal(NotificationEntry entry, int cancellationReason, DismissedByUserStatsCreator statsCreator)1092     public Runnable registerFutureDismissal(NotificationEntry entry, int cancellationReason,
1093             DismissedByUserStatsCreator statsCreator) {
1094         FutureDismissal dismissal = mFutureDismissals.get(entry.getKey());
1095         if (dismissal != null) {
1096             mLogger.logFutureDismissalReused(dismissal);
1097             return dismissal;
1098         }
1099         dismissal = new FutureDismissal(entry, cancellationReason, statsCreator);
1100         mFutureDismissals.put(entry.getKey(), dismissal);
1101         mLogger.logFutureDismissalRegistered(dismissal);
1102         return dismissal;
1103     }
1104 
handleFutureDismissal(NotificationEntry entry)1105     private void handleFutureDismissal(NotificationEntry entry) {
1106         final FutureDismissal futureDismissal = mFutureDismissals.remove(entry.getKey());
1107         if (futureDismissal != null) {
1108             futureDismissal.onSystemServerCancel(entry.mCancellationReason);
1109         }
1110     }
1111 
1112     @Nullable
fetchSummaryToDismiss(NotificationEntry entry)1113     private NotificationEntry fetchSummaryToDismiss(NotificationEntry entry) {
1114         if (isOnlyChildInGroup(entry)) {
1115             String group = entry.getSbn().getGroupKey();
1116             NotificationEntry summary = getGroupSummary(group);
1117             if (summary != null && isDismissable(summary)) return summary;
1118         }
1119         return null;
1120     }
1121 
1122     /** A single method interface that callers can pass in when registering future dismissals */
1123     public interface DismissedByUserStatsCreator {
createDismissedByUserStats(NotificationEntry entry)1124         DismissedByUserStats createDismissedByUserStats(NotificationEntry entry);
1125     }
1126 
1127     /** A class which tracks the double dismissal events coming in from both the system server and
1128      * the ui */
1129     public class FutureDismissal implements Runnable {
1130         private final NotificationEntry mEntry;
1131         private final DismissedByUserStatsCreator mStatsCreator;
1132 
1133         @Nullable
1134         private final NotificationEntry mSummaryToDismiss;
1135         private final String mLabel;
1136 
1137         private boolean mDidRun;
1138         private boolean mDidSystemServerCancel;
1139 
FutureDismissal(NotificationEntry entry, @CancellationReason int cancellationReason, DismissedByUserStatsCreator statsCreator)1140         private FutureDismissal(NotificationEntry entry, @CancellationReason int cancellationReason,
1141                 DismissedByUserStatsCreator statsCreator) {
1142             mEntry = entry;
1143             mStatsCreator = statsCreator;
1144             mSummaryToDismiss = fetchSummaryToDismiss(entry);
1145             mLabel = "<FutureDismissal@" + Integer.toHexString(hashCode())
1146                     + " entry=" + logKey(mEntry)
1147                     + " reason=" + cancellationReasonDebugString(cancellationReason)
1148                     + " summary=" + logKey(mSummaryToDismiss)
1149                     + ">";
1150         }
1151 
1152         /** called when the entry has been removed from the collection */
onSystemServerCancel(@ancellationReason int cancellationReason)1153         public void onSystemServerCancel(@CancellationReason int cancellationReason) {
1154             Assert.isMainThread();
1155             if (mDidSystemServerCancel) {
1156                 mLogger.logFutureDismissalDoubleCancelledByServer(this);
1157                 return;
1158             }
1159             mLogger.logFutureDismissalGotSystemServerCancel(this, cancellationReason);
1160             mDidSystemServerCancel = true;
1161             // TODO: Internally dismiss the summary now instead of waiting for onUiCancel
1162         }
1163 
onUiCancel()1164         private void onUiCancel() {
1165             mFutureDismissals.remove(mEntry.getKey());
1166             final NotificationEntry currentEntry = getEntry(mEntry.getKey());
1167             // generate stats for the entry before dismissing summary, which could affect state
1168             final DismissedByUserStats stats = mStatsCreator.createDismissedByUserStats(mEntry);
1169             // dismiss the summary (if it exists)
1170             if (mSummaryToDismiss != null) {
1171                 final NotificationEntry currentSummary = getEntry(mSummaryToDismiss.getKey());
1172                 if (currentSummary == mSummaryToDismiss) {
1173                     mLogger.logFutureDismissalDismissing(this, "summary");
1174                     dismissNotification(mSummaryToDismiss,
1175                             mStatsCreator.createDismissedByUserStats(mSummaryToDismiss));
1176                 } else {
1177                     mLogger.logFutureDismissalMismatchedEntry(this, "summary", currentSummary);
1178                 }
1179             }
1180             // dismiss this entry (if it is still around)
1181             if (mDidSystemServerCancel) {
1182                 mLogger.logFutureDismissalAlreadyCancelledByServer(this);
1183             } else if (currentEntry == mEntry) {
1184                 mLogger.logFutureDismissalDismissing(this, "entry");
1185                 dismissNotification(mEntry, stats);
1186             } else {
1187                 mLogger.logFutureDismissalMismatchedEntry(this, "entry", currentEntry);
1188             }
1189         }
1190 
1191         /** called when the dismissal should be completed */
1192         @Override
run()1193         public void run() {
1194             Assert.isMainThread();
1195             if (mDidRun) {
1196                 mLogger.logFutureDismissalDoubleRun(this);
1197                 return;
1198             }
1199             mDidRun = true;
1200             onUiCancel();
1201         }
1202 
1203         /** provides a debug label for this instance */
getLabel()1204         public String getLabel() {
1205             return mLabel;
1206         }
1207     }
1208 
1209     @IntDef(prefix = { "REASON_" }, value = {
1210             REASON_NOT_CANCELED,
1211             REASON_UNKNOWN,
1212             REASON_CLICK,
1213             REASON_CANCEL,
1214             REASON_CANCEL_ALL,
1215             REASON_ERROR,
1216             REASON_PACKAGE_CHANGED,
1217             REASON_USER_STOPPED,
1218             REASON_PACKAGE_BANNED,
1219             REASON_APP_CANCEL,
1220             REASON_APP_CANCEL_ALL,
1221             REASON_LISTENER_CANCEL,
1222             REASON_LISTENER_CANCEL_ALL,
1223             REASON_GROUP_SUMMARY_CANCELED,
1224             REASON_GROUP_OPTIMIZATION,
1225             REASON_PACKAGE_SUSPENDED,
1226             REASON_PROFILE_TURNED_OFF,
1227             REASON_UNAUTOBUNDLED,
1228             REASON_CHANNEL_BANNED,
1229             REASON_SNOOZED,
1230             REASON_TIMEOUT,
1231             REASON_CHANNEL_REMOVED,
1232             REASON_CLEAR_DATA,
1233             REASON_ASSISTANT_CANCEL,
1234     })
1235     @Retention(RetentionPolicy.SOURCE)
1236     public @interface CancellationReason {}
1237 
1238     static final int REASON_NOT_CANCELED = -1;
1239     public static final int REASON_UNKNOWN = 0;
1240 
1241     private static final long INITIALIZATION_FORGIVENESS_WINDOW = TimeUnit.SECONDS.toMillis(5);
1242 }
1243