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