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