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