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_CANCEL_ALL;
22 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;
23 import static android.service.notification.NotificationListenerService.REASON_CLICK;
24 import static android.service.notification.NotificationListenerService.REASON_ERROR;
25 import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION;
26 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
27 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL;
28 import static android.service.notification.NotificationListenerService.REASON_LISTENER_CANCEL_ALL;
29 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
30 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED;
31 import static android.service.notification.NotificationListenerService.REASON_PACKAGE_SUSPENDED;
32 import static android.service.notification.NotificationListenerService.REASON_PROFILE_TURNED_OFF;
33 import static android.service.notification.NotificationListenerService.REASON_SNOOZED;
34 import static android.service.notification.NotificationListenerService.REASON_TIMEOUT;
35 import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
36 import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
37 
38 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED;
39 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
40 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
41 
42 import static java.util.Objects.requireNonNull;
43 
44 import android.annotation.IntDef;
45 import android.annotation.MainThread;
46 import android.annotation.Nullable;
47 import android.annotation.UserIdInt;
48 import android.app.Notification;
49 import android.os.RemoteException;
50 import android.os.UserHandle;
51 import android.service.notification.NotificationListenerService;
52 import android.service.notification.NotificationListenerService.Ranking;
53 import android.service.notification.NotificationListenerService.RankingMap;
54 import android.service.notification.StatusBarNotification;
55 import android.util.ArrayMap;
56 import android.util.Pair;
57 
58 import androidx.annotation.NonNull;
59 
60 import com.android.internal.statusbar.IStatusBarService;
61 import com.android.systemui.Dumpable;
62 import com.android.systemui.dump.DumpManager;
63 import com.android.systemui.dump.LogBufferEulogizer;
64 import com.android.systemui.statusbar.FeatureFlags;
65 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
66 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
67 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
68 import com.android.systemui.statusbar.notification.collection.notifcollection.BindEntryEvent;
69 import com.android.systemui.statusbar.notification.collection.notifcollection.CleanUpEntryEvent;
70 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
71 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
72 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryAddedEvent;
73 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryRemovedEvent;
74 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent;
75 import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent;
76 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
77 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
78 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
79 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent;
80 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
81 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingAppliedEvent;
82 import com.android.systemui.statusbar.notification.collection.notifcollection.RankingUpdatedEvent;
83 import com.android.systemui.util.Assert;
84 import com.android.systemui.util.time.SystemClock;
85 
86 import java.io.FileDescriptor;
87 import java.io.PrintWriter;
88 import java.lang.annotation.Retention;
89 import java.lang.annotation.RetentionPolicy;
90 import java.util.ArrayDeque;
91 import java.util.ArrayList;
92 import java.util.Collection;
93 import java.util.Collections;
94 import java.util.List;
95 import java.util.Map;
96 import java.util.Objects;
97 import java.util.Queue;
98 import java.util.concurrent.TimeUnit;
99 
100 import javax.inject.Inject;
101 import javax.inject.Singleton;
102 
103 /**
104  * Keeps a record of all of the "active" notifications, i.e. the notifications that are currently
105  * posted to the phone. This collection is unsorted, ungrouped, and unfiltered. Just because a
106  * notification appears in this collection doesn't mean that it's currently present in the shade
107  * (notifications can be hidden for a variety of reasons). Code that cares about what notifications
108  * are *visible* right now should register listeners later in the pipeline.
109  *
110  * Each notification is represented by a {@link NotificationEntry}, which is itself made up of two
111  * parts: a {@link StatusBarNotification} and a {@link Ranking}. When notifications are updated,
112  * their underlying SBNs and Rankings are swapped out, but the enclosing NotificationEntry (and its
113  * associated key) remain the same. In general, an SBN can only be updated when the notification is
114  * reposted by the source app; Rankings are updated much more often, usually every time there is an
115  * update from any kind from NotificationManager.
116  *
117  * In general, this collection closely mirrors the list maintained by NotificationManager, but it
118  * can occasionally diverge due to lifetime extenders (see
119  * {@link #addNotificationLifetimeExtender(NotifLifetimeExtender)}).
120  *
121  * Interested parties can register listeners
122  * ({@link #addCollectionListener(NotifCollectionListener)}) to be informed when notifications
123  * events occur.
124  */
125 @MainThread
126 @Singleton
127 public class NotifCollection implements Dumpable {
128     private final IStatusBarService mStatusBarService;
129     private final SystemClock mClock;
130     private final FeatureFlags mFeatureFlags;
131     private final NotifCollectionLogger mLogger;
132     private final LogBufferEulogizer mEulogizer;
133 
134     private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
135     private final Collection<NotificationEntry> mReadOnlyNotificationSet =
136             Collections.unmodifiableCollection(mNotificationSet.values());
137 
138     @Nullable private CollectionReadyForBuildListener mBuildListener;
139     private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
140     private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
141     private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
142 
143     private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
144 
145     private boolean mAttached = false;
146     private boolean mAmDispatchingToOtherCode;
147     private long mInitializedTimestamp = 0;
148 
149     @Inject
NotifCollection( IStatusBarService statusBarService, SystemClock clock, FeatureFlags featureFlags, NotifCollectionLogger logger, LogBufferEulogizer logBufferEulogizer, DumpManager dumpManager)150     public NotifCollection(
151             IStatusBarService statusBarService,
152             SystemClock clock,
153             FeatureFlags featureFlags,
154             NotifCollectionLogger logger,
155             LogBufferEulogizer logBufferEulogizer,
156             DumpManager dumpManager) {
157         Assert.isMainThread();
158         mStatusBarService = statusBarService;
159         mClock = clock;
160         mFeatureFlags = featureFlags;
161         mLogger = logger;
162         mEulogizer = logBufferEulogizer;
163 
164         dumpManager.registerDumpable(TAG, this);
165     }
166 
167     /** Initializes the NotifCollection and registers it to receive notification events. */
attach(GroupCoalescer groupCoalescer)168     public void attach(GroupCoalescer groupCoalescer) {
169         Assert.isMainThread();
170         if (mAttached) {
171             throw new RuntimeException("attach() called twice");
172         }
173         mAttached = true;
174 
175         groupCoalescer.setNotificationHandler(mNotifHandler);
176     }
177 
178     /**
179      * Sets the class responsible for converting the collection into the list of currently-visible
180      * notifications.
181      */
setBuildListener(CollectionReadyForBuildListener buildListener)182     void setBuildListener(CollectionReadyForBuildListener buildListener) {
183         Assert.isMainThread();
184         mBuildListener = buildListener;
185     }
186 
187     /** @see NotifPipeline#getAllNotifs() */
getAllNotifs()188     Collection<NotificationEntry> getAllNotifs() {
189         Assert.isMainThread();
190         return mReadOnlyNotificationSet;
191     }
192 
193     /** @see NotifPipeline#addCollectionListener(NotifCollectionListener) */
addCollectionListener(NotifCollectionListener listener)194     void addCollectionListener(NotifCollectionListener listener) {
195         Assert.isMainThread();
196         mNotifCollectionListeners.add(listener);
197     }
198 
199     /** @see NotifPipeline#addNotificationLifetimeExtender(NotifLifetimeExtender) */
addNotificationLifetimeExtender(NotifLifetimeExtender extender)200     void addNotificationLifetimeExtender(NotifLifetimeExtender extender) {
201         Assert.isMainThread();
202         checkForReentrantCall();
203         if (mLifetimeExtenders.contains(extender)) {
204             throw new IllegalArgumentException("Extender " + extender + " already added.");
205         }
206         mLifetimeExtenders.add(extender);
207         extender.setCallback(this::onEndLifetimeExtension);
208     }
209 
210     /** @see NotifPipeline#addNotificationDismissInterceptor(NotifDismissInterceptor) */
addNotificationDismissInterceptor(NotifDismissInterceptor interceptor)211     void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) {
212         Assert.isMainThread();
213         checkForReentrantCall();
214         if (mDismissInterceptors.contains(interceptor)) {
215             throw new IllegalArgumentException("Interceptor " + interceptor + " already added.");
216         }
217         mDismissInterceptors.add(interceptor);
218         interceptor.setCallback(this::onEndDismissInterception);
219     }
220 
221     /**
222      * Dismisses multiple notifications on behalf of the user.
223      */
dismissNotifications( List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss)224     public void dismissNotifications(
225             List<Pair<NotificationEntry, DismissedByUserStats>> entriesToDismiss) {
226         Assert.isMainThread();
227         checkForReentrantCall();
228 
229         final List<NotificationEntry> entriesToLocallyDismiss = new ArrayList<>();
230         for (int i = 0; i < entriesToDismiss.size(); i++) {
231             NotificationEntry entry = entriesToDismiss.get(i).first;
232             DismissedByUserStats stats = entriesToDismiss.get(i).second;
233 
234             requireNonNull(stats);
235             if (entry != mNotificationSet.get(entry.getKey())) {
236                 throw mEulogizer.record(
237                         new IllegalStateException("Invalid entry: " + entry.getKey()));
238             }
239 
240             if (entry.getDismissState() == DISMISSED) {
241                 continue;
242             }
243 
244             updateDismissInterceptors(entry);
245             if (isDismissIntercepted(entry)) {
246                 mLogger.logNotifDismissedIntercepted(entry.getKey());
247                 continue;
248             }
249 
250             entriesToLocallyDismiss.add(entry);
251             if (!isCanceled(entry)) {
252                 // send message to system server if this notification hasn't already been cancelled
253                 try {
254                     mStatusBarService.onNotificationClear(
255                             entry.getSbn().getPackageName(),
256                             entry.getSbn().getTag(),
257                             entry.getSbn().getId(),
258                             entry.getSbn().getUser().getIdentifier(),
259                             entry.getSbn().getKey(),
260                             stats.dismissalSurface,
261                             stats.dismissalSentiment,
262                             stats.notificationVisibility);
263                 } catch (RemoteException e) {
264                     // system process is dead if we're here.
265                     mLogger.logRemoteExceptionOnNotificationClear(entry.getKey(), e);
266                 }
267             }
268         }
269 
270         locallyDismissNotifications(entriesToLocallyDismiss);
271         dispatchEventsAndRebuildList();
272     }
273 
274     /**
275      * Dismisses a single notification on behalf of the user.
276      */
dismissNotification( NotificationEntry entry, @NonNull DismissedByUserStats stats)277     public void dismissNotification(
278             NotificationEntry entry,
279             @NonNull DismissedByUserStats stats) {
280         dismissNotifications(List.of(new Pair<>(entry, stats)));
281     }
282 
283     /**
284      * Dismisses all clearable notifications for a given userid on behalf of the user.
285      */
dismissAllNotifications(@serIdInt int userId)286     public void dismissAllNotifications(@UserIdInt int userId) {
287         Assert.isMainThread();
288         checkForReentrantCall();
289 
290         mLogger.logDismissAll(userId);
291 
292         try {
293             mStatusBarService.onClearAllNotifications(userId);
294         } catch (RemoteException e) {
295             // system process is dead if we're here.
296             mLogger.logRemoteExceptionOnClearAllNotifications(e);
297         }
298 
299         final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
300         for (int i = entries.size() - 1; i >= 0; i--) {
301             NotificationEntry entry = entries.get(i);
302             if (!shouldDismissOnClearAll(entry, userId)) {
303                 // system server won't be removing these notifications, but we still give dismiss
304                 // interceptors the chance to filter the notification
305                 updateDismissInterceptors(entry);
306                 if (isDismissIntercepted(entry)) {
307                     mLogger.logNotifClearAllDismissalIntercepted(entry.getKey());
308                 }
309                 entries.remove(i);
310             }
311         }
312 
313         locallyDismissNotifications(entries);
314         dispatchEventsAndRebuildList();
315     }
316 
317     /**
318      * Optimistically marks the given notifications as dismissed -- we'll wait for the signal
319      * from system server before removing it from our notification set.
320      */
locallyDismissNotifications(List<NotificationEntry> entries)321     private void locallyDismissNotifications(List<NotificationEntry> entries) {
322         final List<NotificationEntry> canceledEntries = new ArrayList<>();
323 
324         for (int i = 0; i < entries.size(); i++) {
325             NotificationEntry entry = entries.get(i);
326 
327             entry.setDismissState(DISMISSED);
328             mLogger.logNotifDismissed(entry.getKey());
329 
330             if (isCanceled(entry)) {
331                 canceledEntries.add(entry);
332             } else {
333                 // Mark any children as dismissed as system server will auto-dismiss them as well
334                 if (entry.getSbn().getNotification().isGroupSummary()) {
335                     for (NotificationEntry otherEntry : mNotificationSet.values()) {
336                         if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) {
337                             otherEntry.setDismissState(PARENT_DISMISSED);
338                             mLogger.logChildDismissed(otherEntry);
339                             if (isCanceled(otherEntry)) {
340                                 canceledEntries.add(otherEntry);
341                             }
342                         }
343                     }
344                 }
345             }
346         }
347 
348         // Immediately remove any dismissed notifs that have already been canceled by system server
349         // (probably due to being lifetime-extended up until this point).
350         for (NotificationEntry canceledEntry : canceledEntries) {
351             mLogger.logDismissOnAlreadyCanceledEntry(canceledEntry);
352             tryRemoveNotification(canceledEntry);
353         }
354     }
355 
onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)356     private void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
357         Assert.isMainThread();
358 
359         postNotification(sbn, requireRanking(rankingMap, sbn.getKey()));
360         applyRanking(rankingMap);
361         dispatchEventsAndRebuildList();
362     }
363 
onNotificationGroupPosted(List<CoalescedEvent> batch)364     private void onNotificationGroupPosted(List<CoalescedEvent> batch) {
365         Assert.isMainThread();
366 
367         mLogger.logNotifGroupPosted(batch.get(0).getSbn().getGroupKey(), batch.size());
368 
369         for (CoalescedEvent event : batch) {
370             postNotification(event.getSbn(), event.getRanking());
371         }
372         dispatchEventsAndRebuildList();
373     }
374 
onNotificationRemoved( StatusBarNotification sbn, RankingMap rankingMap, int reason)375     private void onNotificationRemoved(
376             StatusBarNotification sbn,
377             RankingMap rankingMap,
378             int reason) {
379         Assert.isMainThread();
380 
381         mLogger.logNotifRemoved(sbn.getKey(), reason);
382 
383         final NotificationEntry entry = mNotificationSet.get(sbn.getKey());
384         if (entry == null) {
385             // TODO (b/160008901): Throw an exception here
386             mLogger.logNoNotificationToRemoveWithKey(sbn.getKey());
387             return;
388         }
389 
390         entry.mCancellationReason = reason;
391         tryRemoveNotification(entry);
392         applyRanking(rankingMap);
393         dispatchEventsAndRebuildList();
394     }
395 
onNotificationRankingUpdate(RankingMap rankingMap)396     private void onNotificationRankingUpdate(RankingMap rankingMap) {
397         Assert.isMainThread();
398         mEventQueue.add(new RankingUpdatedEvent(rankingMap));
399         applyRanking(rankingMap);
400         dispatchEventsAndRebuildList();
401     }
402 
onNotificationsInitialized()403     private void onNotificationsInitialized() {
404         mInitializedTimestamp = mClock.uptimeMillis();
405     }
406 
postNotification( StatusBarNotification sbn, Ranking ranking)407     private void postNotification(
408             StatusBarNotification sbn,
409             Ranking ranking) {
410         NotificationEntry entry = mNotificationSet.get(sbn.getKey());
411 
412         if (entry == null) {
413             // A new notification!
414             entry = new NotificationEntry(sbn, ranking, mClock.uptimeMillis());
415             mEventQueue.add(new InitEntryEvent(entry));
416             mEventQueue.add(new BindEntryEvent(entry, sbn));
417             mNotificationSet.put(sbn.getKey(), entry);
418 
419             mLogger.logNotifPosted(sbn.getKey());
420             mEventQueue.add(new EntryAddedEvent(entry));
421 
422         } else {
423             // Update to an existing entry
424 
425             // Notification is updated so it is essentially re-added and thus alive again, so we
426             // can reset its state.
427             // TODO: If a coalesced event ever gets here, it's possible to lose track of children,
428             //  since their rankings might have been updated earlier (and thus we may no longer
429             //  think a child is associated with this locally-dismissed entry).
430             cancelLocalDismissal(entry);
431             cancelLifetimeExtension(entry);
432             cancelDismissInterception(entry);
433             entry.mCancellationReason = REASON_NOT_CANCELED;
434 
435             entry.setSbn(sbn);
436             mEventQueue.add(new BindEntryEvent(entry, sbn));
437 
438             mLogger.logNotifUpdated(sbn.getKey());
439             mEventQueue.add(new EntryUpdatedEvent(entry));
440         }
441     }
442 
443     /**
444      * Tries to remove a notification from the notification set. This removal may be blocked by
445      * lifetime extenders. Does not trigger a rebuild of the list; caller must do that manually.
446      *
447      * @return True if the notification was removed, false otherwise.
448      */
tryRemoveNotification(NotificationEntry entry)449     private boolean tryRemoveNotification(NotificationEntry entry) {
450         if (mNotificationSet.get(entry.getKey()) != entry) {
451             throw mEulogizer.record(
452                     new IllegalStateException("No notification to remove with key "
453                             + entry.getKey()));
454         }
455 
456         if (!isCanceled(entry)) {
457             throw mEulogizer.record(
458                     new IllegalStateException("Cannot remove notification " + entry.getKey()
459                             + ": has not been marked for removal"));
460         }
461 
462         if (isDismissedByUser(entry)) {
463             // User-dismissed notifications cannot be lifetime-extended
464             cancelLifetimeExtension(entry);
465         } else {
466             updateLifetimeExtension(entry);
467         }
468 
469         if (!isLifetimeExtended(entry)) {
470             mLogger.logNotifReleased(entry.getKey());
471             mNotificationSet.remove(entry.getKey());
472             cancelDismissInterception(entry);
473             mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason));
474             mEventQueue.add(new CleanUpEntryEvent(entry));
475             return true;
476         } else {
477             return false;
478         }
479     }
480 
applyRanking(@onNull RankingMap rankingMap)481     private void applyRanking(@NonNull RankingMap rankingMap) {
482         for (NotificationEntry entry : mNotificationSet.values()) {
483             if (!isCanceled(entry)) {
484 
485                 // TODO: (b/148791039) We should crash if we are ever handed a ranking with
486                 //  incomplete entries. Right now, there's a race condition in NotificationListener
487                 //  that means this might occur when SystemUI is starting up.
488                 Ranking ranking = new Ranking();
489                 if (rankingMap.getRanking(entry.getKey(), ranking)) {
490                     entry.setRanking(ranking);
491 
492                     // TODO: (b/145659174) update the sbn's overrideGroupKey in
493                     //  NotificationEntry.setRanking instead of here once we fully migrate to the
494                     //  NewNotifPipeline
495                     if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
496                         final String newOverrideGroupKey = ranking.getOverrideGroupKey();
497                         if (!Objects.equals(entry.getSbn().getOverrideGroupKey(),
498                                 newOverrideGroupKey)) {
499                             entry.getSbn().setOverrideGroupKey(newOverrideGroupKey);
500                         }
501                     }
502                 } else {
503                     mLogger.logRankingMissing(entry.getKey(), rankingMap);
504                 }
505             }
506         }
507         mEventQueue.add(new RankingAppliedEvent());
508     }
509 
dispatchEventsAndRebuildList()510     private void dispatchEventsAndRebuildList() {
511         mAmDispatchingToOtherCode = true;
512         while (!mEventQueue.isEmpty()) {
513             mEventQueue.remove().dispatchTo(mNotifCollectionListeners);
514         }
515         mAmDispatchingToOtherCode = false;
516 
517         if (mBuildListener != null) {
518             mBuildListener.onBuildList(mReadOnlyNotificationSet);
519         }
520     }
521 
onEndLifetimeExtension(NotifLifetimeExtender extender, NotificationEntry entry)522     private void onEndLifetimeExtension(NotifLifetimeExtender extender, NotificationEntry entry) {
523         Assert.isMainThread();
524         if (!mAttached) {
525             return;
526         }
527         checkForReentrantCall();
528 
529         if (!entry.mLifetimeExtenders.remove(extender)) {
530             throw mEulogizer.record(new IllegalStateException(
531                     String.format(
532                             "Cannot end lifetime extension for extender \"%s\" (%s)",
533                             extender.getName(),
534                             extender)));
535         }
536 
537         mLogger.logLifetimeExtensionEnded(
538                 entry.getKey(),
539                 extender,
540                 entry.mLifetimeExtenders.size());
541 
542         if (!isLifetimeExtended(entry)) {
543             if (tryRemoveNotification(entry)) {
544                 dispatchEventsAndRebuildList();
545             }
546         }
547     }
548 
cancelLifetimeExtension(NotificationEntry entry)549     private void cancelLifetimeExtension(NotificationEntry entry) {
550         mAmDispatchingToOtherCode = true;
551         for (NotifLifetimeExtender extender : entry.mLifetimeExtenders) {
552             extender.cancelLifetimeExtension(entry);
553         }
554         mAmDispatchingToOtherCode = false;
555         entry.mLifetimeExtenders.clear();
556     }
557 
isLifetimeExtended(NotificationEntry entry)558     private boolean isLifetimeExtended(NotificationEntry entry) {
559         return entry.mLifetimeExtenders.size() > 0;
560     }
561 
updateLifetimeExtension(NotificationEntry entry)562     private void updateLifetimeExtension(NotificationEntry entry) {
563         entry.mLifetimeExtenders.clear();
564         mAmDispatchingToOtherCode = true;
565         for (NotifLifetimeExtender extender : mLifetimeExtenders) {
566             if (extender.shouldExtendLifetime(entry, entry.mCancellationReason)) {
567                 mLogger.logLifetimeExtended(entry.getKey(), extender);
568                 entry.mLifetimeExtenders.add(extender);
569             }
570         }
571         mAmDispatchingToOtherCode = false;
572     }
573 
updateDismissInterceptors(@onNull NotificationEntry entry)574     private void updateDismissInterceptors(@NonNull NotificationEntry entry) {
575         entry.mDismissInterceptors.clear();
576         mAmDispatchingToOtherCode = true;
577         for (NotifDismissInterceptor interceptor : mDismissInterceptors) {
578             if (interceptor.shouldInterceptDismissal(entry)) {
579                 entry.mDismissInterceptors.add(interceptor);
580             }
581         }
582         mAmDispatchingToOtherCode = false;
583     }
584 
cancelLocalDismissal(NotificationEntry entry)585     private void cancelLocalDismissal(NotificationEntry entry) {
586         if (isDismissedByUser(entry)) {
587             entry.setDismissState(NOT_DISMISSED);
588             if (entry.getSbn().getNotification().isGroupSummary()) {
589                 for (NotificationEntry otherEntry : mNotificationSet.values()) {
590                     if (otherEntry.getSbn().getGroupKey().equals(entry.getSbn().getGroupKey())
591                             && otherEntry.getDismissState() == PARENT_DISMISSED) {
592                         otherEntry.setDismissState(NOT_DISMISSED);
593                     }
594                 }
595             }
596         }
597     }
598 
onEndDismissInterception( NotifDismissInterceptor interceptor, NotificationEntry entry, @NonNull DismissedByUserStats stats)599     private void onEndDismissInterception(
600             NotifDismissInterceptor interceptor,
601             NotificationEntry entry,
602             @NonNull DismissedByUserStats stats) {
603         Assert.isMainThread();
604         if (!mAttached) {
605             return;
606         }
607         checkForReentrantCall();
608 
609         if (!entry.mDismissInterceptors.remove(interceptor)) {
610             throw mEulogizer.record(new IllegalStateException(
611                     String.format(
612                             "Cannot end dismiss interceptor for interceptor \"%s\" (%s)",
613                             interceptor.getName(),
614                             interceptor)));
615         }
616 
617         if (!isDismissIntercepted(entry)) {
618             dismissNotification(entry, stats);
619         }
620     }
621 
cancelDismissInterception(NotificationEntry entry)622     private void cancelDismissInterception(NotificationEntry entry) {
623         mAmDispatchingToOtherCode = true;
624         for (NotifDismissInterceptor interceptor : entry.mDismissInterceptors) {
625             interceptor.cancelDismissInterception(entry);
626         }
627         mAmDispatchingToOtherCode = false;
628         entry.mDismissInterceptors.clear();
629     }
630 
isDismissIntercepted(NotificationEntry entry)631     private boolean isDismissIntercepted(NotificationEntry entry) {
632         return entry.mDismissInterceptors.size() > 0;
633     }
634 
checkForReentrantCall()635     private void checkForReentrantCall() {
636         if (mAmDispatchingToOtherCode) {
637             throw mEulogizer.record(new IllegalStateException("Reentrant call detected"));
638         }
639     }
640 
641     // While the NotificationListener is connecting to NotificationManager, there is a short period
642     // during which it's possible for us to receive events about notifications we don't yet know
643     // about (or that otherwise don't make sense). Until that race condition is fixed, we create a
644     // "forgiveness window" of five seconds during which we won't crash if we receive nonsensical
645     // messages from system server.
crashIfNotInitializing(RuntimeException exception)646     private void crashIfNotInitializing(RuntimeException exception) {
647         final boolean isRecentlyInitialized = mInitializedTimestamp == 0
648                 || mClock.uptimeMillis() - mInitializedTimestamp
649                         < INITIALIZATION_FORGIVENESS_WINDOW;
650 
651         if (isRecentlyInitialized) {
652             mLogger.logIgnoredError(exception.getMessage());
653         } else {
654             throw mEulogizer.record(exception);
655         }
656     }
657 
658     private static Ranking requireRanking(RankingMap rankingMap, String key) {
659         // TODO: Modify RankingMap so that we don't have to make a copy here
660         Ranking ranking = new Ranking();
661         if (!rankingMap.getRanking(key, ranking)) {
662             throw new IllegalArgumentException("Ranking map doesn't contain key: " + key);
663         }
664         return ranking;
665     }
666 
667     /**
668      * True if the notification has been canceled by system server. Usually, such notifications are
669      * immediately removed from the collection, but can sometimes stick around due to lifetime
670      * extenders.
671      */
672     private static boolean isCanceled(NotificationEntry entry) {
673         return entry.mCancellationReason != REASON_NOT_CANCELED;
674     }
675 
676     private static boolean isDismissedByUser(NotificationEntry entry) {
677         return entry.getDismissState() != NOT_DISMISSED;
678     }
679 
680     /**
681      * When a group summary is dismissed, NotificationManager will also try to dismiss its children.
682      * Returns true if we think dismissing the group summary with group key
683      * <code>dismissedGroupKey</code> will cause NotificationManager to also dismiss
684      * <code>entry</code>.
685      *
686      * See NotificationManager.cancelGroupChildrenByListLocked() for corresponding code.
687      */
688     private static boolean shouldAutoDismissChildren(
689             NotificationEntry entry,
690             String dismissedGroupKey) {
691         return entry.getSbn().getGroupKey().equals(dismissedGroupKey)
692                 && !entry.getSbn().getNotification().isGroupSummary()
693                 && !hasFlag(entry, Notification.FLAG_FOREGROUND_SERVICE)
694                 && !hasFlag(entry, Notification.FLAG_BUBBLE)
695                 && entry.getDismissState() != DISMISSED;
696     }
697 
698     /**
699      * When the user 'clears all notifications' through SystemUI, NotificationManager will not
700      * dismiss unclearable notifications.
701      * @return true if we think NotificationManager will dismiss the entry when asked to
702      * cancel this notification with {@link NotificationListenerService#REASON_CANCEL_ALL}
703      *
704      * See NotificationManager.cancelAllLocked for corresponding code.
705      */
706     private static boolean shouldDismissOnClearAll(
707             NotificationEntry entry,
708             @UserIdInt int userId) {
709         return userIdMatches(entry, userId)
710                 && entry.isClearable()
711                 && !hasFlag(entry, Notification.FLAG_BUBBLE)
712                 && entry.getDismissState() != DISMISSED;
713     }
714 
715     private static boolean hasFlag(NotificationEntry entry, int flag) {
716         return (entry.getSbn().getNotification().flags & flag) != 0;
717     }
718 
719     /**
720      * Determine whether the userId applies to the notification in question, either because
721      * they match exactly, or one of them is USER_ALL (which is treated as a wildcard).
722      *
723      * See NotificationManager#notificationMatchesUserId
724      */
725     private static boolean userIdMatches(NotificationEntry entry, int userId) {
726         return userId == UserHandle.USER_ALL
727                 || entry.getSbn().getUser().getIdentifier() == UserHandle.USER_ALL
728                 || entry.getSbn().getUser().getIdentifier() == userId;
729     }
730 
731     @Override
732     public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) {
733         final List<NotificationEntry> entries = new ArrayList<>(getAllNotifs());
734 
735         pw.println("\t" + TAG + " unsorted/unfiltered notifications:");
736         if (entries.size() == 0) {
737             pw.println("\t\t None");
738         }
739         pw.println(
740                 ListDumper.dumpList(
741                         entries,
742                         true,
743                         "\t\t"));
744     }
745 
746     private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() {
747         @Override
748         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
749             NotifCollection.this.onNotificationPosted(sbn, rankingMap);
750         }
751 
752         @Override
753         public void onNotificationBatchPosted(List<CoalescedEvent> events) {
754             NotifCollection.this.onNotificationGroupPosted(events);
755         }
756 
757         @Override
758         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
759             NotifCollection.this.onNotificationRemoved(sbn, rankingMap, REASON_UNKNOWN);
760         }
761 
762         @Override
763         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
764                 int reason) {
765             NotifCollection.this.onNotificationRemoved(sbn, rankingMap, reason);
766         }
767 
768         @Override
769         public void onNotificationRankingUpdate(RankingMap rankingMap) {
770             NotifCollection.this.onNotificationRankingUpdate(rankingMap);
771         }
772 
773         @Override
774         public void onNotificationsInitialized() {
775             NotifCollection.this.onNotificationsInitialized();
776         }
777     };
778 
779     private static final String TAG = "NotifCollection";
780 
781     @IntDef(prefix = { "REASON_" }, value = {
782             REASON_NOT_CANCELED,
783             REASON_UNKNOWN,
784             REASON_CLICK,
785             REASON_CANCEL_ALL,
786             REASON_ERROR,
787             REASON_PACKAGE_CHANGED,
788             REASON_USER_STOPPED,
789             REASON_PACKAGE_BANNED,
790             REASON_APP_CANCEL,
791             REASON_APP_CANCEL_ALL,
792             REASON_LISTENER_CANCEL,
793             REASON_LISTENER_CANCEL_ALL,
794             REASON_GROUP_SUMMARY_CANCELED,
795             REASON_GROUP_OPTIMIZATION,
796             REASON_PACKAGE_SUSPENDED,
797             REASON_PROFILE_TURNED_OFF,
798             REASON_UNAUTOBUNDLED,
799             REASON_CHANNEL_BANNED,
800             REASON_SNOOZED,
801             REASON_TIMEOUT,
802     })
803     @Retention(RetentionPolicy.SOURCE)
804     public @interface CancellationReason {}
805 
806     static final int REASON_NOT_CANCELED = -1;
807     public static final int REASON_UNKNOWN = 0;
808 
809     private static final long INITIALIZATION_FORGIVENESS_WINDOW = TimeUnit.SECONDS.toMillis(5);
810 }
811