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