1 /*
2  * Copyright (C) 2017 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 package com.android.systemui.statusbar.notification;
17 
18 import static android.service.notification.NotificationListenerService.REASON_ERROR;
19 
20 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_UNKNOWN;
21 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.Notification;
26 import android.os.RemoteException;
27 import android.os.SystemClock;
28 import android.service.notification.NotificationListenerService;
29 import android.service.notification.NotificationListenerService.Ranking;
30 import android.service.notification.NotificationListenerService.RankingMap;
31 import android.service.notification.StatusBarNotification;
32 import android.util.ArrayMap;
33 import android.util.ArraySet;
34 import android.util.Log;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.internal.statusbar.IStatusBarService;
38 import com.android.internal.statusbar.NotificationVisibility;
39 import com.android.systemui.Dumpable;
40 import com.android.systemui.dump.DumpManager;
41 import com.android.systemui.flags.FeatureFlags;
42 import com.android.systemui.statusbar.NotificationLifetimeExtender;
43 import com.android.systemui.statusbar.NotificationListener;
44 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
45 import com.android.systemui.statusbar.NotificationPresenter;
46 import com.android.systemui.statusbar.NotificationRemoteInputManager;
47 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
48 import com.android.systemui.statusbar.NotificationUiAdjustment;
49 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
50 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
51 import com.android.systemui.statusbar.notification.collection.legacy.LegacyNotificationRanker;
52 import com.android.systemui.statusbar.notification.collection.legacy.LegacyNotificationRankerStub;
53 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
54 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
55 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
56 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
57 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
58 import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
59 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
60 import com.android.systemui.util.Assert;
61 import com.android.systemui.util.leak.LeakDetector;
62 
63 import java.io.FileDescriptor;
64 import java.io.PrintWriter;
65 import java.util.ArrayList;
66 import java.util.Collection;
67 import java.util.Collections;
68 import java.util.HashMap;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.Set;
72 
73 import dagger.Lazy;
74 
75 /**
76  * NotificationEntryManager is responsible for the adding, removing, and updating of
77  * {@link NotificationEntry}s. It also handles tasks such as their inflation and their interaction
78  * with other Notification.*Manager objects.
79  *
80  * We track notification entries through this lifecycle:
81  *      1. Pending
82  *      2. Active
83  *      3. Sorted / filtered (visible)
84  *
85  * Every entry spends some amount of time in the pending state, while it is being inflated. Once
86  * inflated, an entry moves into the active state, where it _could_ potentially be shown to the
87  * user. After an entry makes its way into the active state, we sort and filter the entire set to
88  * repopulate the visible set.
89  *
90  * There are a few different things that other classes may be interested in, and most of them
91  * involve the current set of notifications. Here's a brief overview of things you may want to know:
92  * @see #getVisibleNotifications() for the visible set
93  * @see #getActiveNotificationUnfiltered(String) to check if a key exists
94  * @see #getPendingNotificationsIterator() for an iterator over the pending notifications
95  * @see #getPendingOrActiveNotif(String) to find a notification exists for that key in any list
96  * @see #getActiveNotificationsForCurrentUser() to see every notification that the current user owns
97  */
98 public class NotificationEntryManager implements
99         CommonNotifCollection,
100         Dumpable,
101         VisualStabilityManager.Callback {
102 
103     private final NotificationEntryManagerLogger mLogger;
104     private final NotificationGroupManagerLegacy mGroupManager;
105     private final FeatureFlags mFeatureFlags;
106     private final Lazy<NotificationRowBinder> mNotificationRowBinderLazy;
107     private final Lazy<NotificationRemoteInputManager> mRemoteInputManagerLazy;
108     private final LeakDetector mLeakDetector;
109     private final ForegroundServiceDismissalFeatureController mFgsFeatureController;
110     private final IStatusBarService mStatusBarService;
111     private final DumpManager mDumpManager;
112 
113     private final Set<NotificationEntry> mAllNotifications = new ArraySet<>();
114     private final Set<NotificationEntry> mReadOnlyAllNotifications =
115             Collections.unmodifiableSet(mAllNotifications);
116 
117     /** Pending notifications are ones awaiting inflation */
118     @VisibleForTesting
119     protected final HashMap<String, NotificationEntry> mPendingNotifications = new HashMap<>();
120     /**
121      * Active notifications have been inflated / prepared and could become visible, but may get
122      * filtered out if for instance they are not for the current user
123      */
124     private final ArrayMap<String, NotificationEntry> mActiveNotifications = new ArrayMap<>();
125     @VisibleForTesting
126     /** This is the list of "active notifications for this user in this context" */
127     protected final ArrayList<NotificationEntry> mSortedAndFiltered = new ArrayList<>();
128     private final List<NotificationEntry> mReadOnlyNotifications =
129             Collections.unmodifiableList(mSortedAndFiltered);
130 
131     private final Map<NotificationEntry, NotificationLifetimeExtender> mRetainedNotifications =
132             new ArrayMap<>();
133 
134     private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
135 
136     private LegacyNotificationRanker mRanker = new LegacyNotificationRankerStub();
137     private NotificationPresenter mPresenter;
138     private RankingMap mLatestRankingMap;
139 
140     @VisibleForTesting
141     final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
142             = new ArrayList<>();
143     private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>();
144     private final List<NotificationRemoveInterceptor> mRemoveInterceptors = new ArrayList<>();
145 
146     /**
147      * Injected constructor. See {@link NotificationsModule}.
148      */
NotificationEntryManager( NotificationEntryManagerLogger logger, NotificationGroupManagerLegacy groupManager, FeatureFlags featureFlags, Lazy<NotificationRowBinder> notificationRowBinderLazy, Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy, LeakDetector leakDetector, ForegroundServiceDismissalFeatureController fgsFeatureController, IStatusBarService statusBarService, DumpManager dumpManager )149     public NotificationEntryManager(
150             NotificationEntryManagerLogger logger,
151             NotificationGroupManagerLegacy groupManager,
152             FeatureFlags featureFlags,
153             Lazy<NotificationRowBinder> notificationRowBinderLazy,
154             Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
155             LeakDetector leakDetector,
156             ForegroundServiceDismissalFeatureController fgsFeatureController,
157             IStatusBarService statusBarService,
158             DumpManager dumpManager
159     ) {
160         mLogger = logger;
161         mGroupManager = groupManager;
162         mFeatureFlags = featureFlags;
163         mNotificationRowBinderLazy = notificationRowBinderLazy;
164         mRemoteInputManagerLazy = notificationRemoteInputManagerLazy;
165         mLeakDetector = leakDetector;
166         mFgsFeatureController = fgsFeatureController;
167         mStatusBarService = statusBarService;
168         mDumpManager = dumpManager;
169     }
170 
171     /** Once called, the NEM will start processing notification events from system server. */
initialize( NotificationListener notificationListener, LegacyNotificationRanker ranker)172     public void initialize(
173             NotificationListener notificationListener,
174             LegacyNotificationRanker ranker) {
175         mRanker = ranker;
176         notificationListener.addNotificationHandler(mNotifListener);
177         mDumpManager.registerDumpable(this);
178     }
179 
180     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)181     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
182         pw.println("NotificationEntryManager state:");
183         pw.println("  mAllNotifications=");
184         if (mAllNotifications.size() == 0) {
185             pw.println("null");
186         } else {
187             int i = 0;
188             for (NotificationEntry entry : mAllNotifications) {
189                 dumpEntry(pw, "  ", i, entry);
190                 i++;
191             }
192         }
193         pw.print("  mPendingNotifications=");
194         if (mPendingNotifications.size() == 0) {
195             pw.println("null");
196         } else {
197             for (NotificationEntry entry : mPendingNotifications.values()) {
198                 pw.println(entry.getSbn());
199             }
200         }
201         pw.println("  Remove interceptors registered:");
202         for (NotificationRemoveInterceptor interceptor : mRemoveInterceptors) {
203             pw.println("    " + interceptor.getClass().getSimpleName());
204         }
205         pw.println("  Lifetime extenders registered:");
206         for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
207             pw.println("    " + extender.getClass().getSimpleName());
208         }
209         pw.println("  Lifetime-extended notifications:");
210         if (mRetainedNotifications.isEmpty()) {
211             pw.println("    None");
212         } else {
213             for (Map.Entry<NotificationEntry, NotificationLifetimeExtender> entry
214                     : mRetainedNotifications.entrySet()) {
215                 pw.println("    " + entry.getKey().getSbn() + " retained by "
216                         + entry.getValue().getClass().getName());
217             }
218         }
219     }
220 
221     /** Adds a {@link NotificationEntryListener}. */
addNotificationEntryListener(NotificationEntryListener listener)222     public void addNotificationEntryListener(NotificationEntryListener listener) {
223         mNotificationEntryListeners.add(listener);
224     }
225 
226     /**
227      * Removes a {@link NotificationEntryListener} previously registered via
228      * {@link #addNotificationEntryListener(NotificationEntryListener)}.
229      */
removeNotificationEntryListener(NotificationEntryListener listener)230     public void removeNotificationEntryListener(NotificationEntryListener listener) {
231         mNotificationEntryListeners.remove(listener);
232     }
233 
234     /** Add a {@link NotificationRemoveInterceptor}. */
addNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor)235     public void addNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
236         mRemoveInterceptors.add(interceptor);
237     }
238 
239     /** Remove a {@link NotificationRemoveInterceptor} */
removeNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor)240     public void removeNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
241         mRemoveInterceptors.remove(interceptor);
242     }
243 
setUpWithPresenter(NotificationPresenter presenter)244     public void setUpWithPresenter(NotificationPresenter presenter) {
245         mPresenter = presenter;
246     }
247 
248     /** Adds multiple {@link NotificationLifetimeExtender}s. */
addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders)249     public void addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders) {
250         for (NotificationLifetimeExtender extender : extenders) {
251             addNotificationLifetimeExtender(extender);
252         }
253     }
254 
255     /** Adds a {@link NotificationLifetimeExtender}. */
addNotificationLifetimeExtender(NotificationLifetimeExtender extender)256     public void addNotificationLifetimeExtender(NotificationLifetimeExtender extender) {
257         mNotificationLifetimeExtenders.add(extender);
258         extender.setCallback(key -> removeNotification(key, mLatestRankingMap,
259                 UNDEFINED_DISMISS_REASON));
260     }
261 
262     @Override
onChangeAllowed()263     public void onChangeAllowed() {
264         updateNotifications("reordering is now allowed");
265     }
266 
267     /**
268      * User requests a notification to be removed.
269      *
270      * @param n the notification to remove.
271      * @param reason why it is being removed e.g. {@link NotificationListenerService#REASON_CANCEL},
272      *               or 0 if unknown.
273      */
performRemoveNotification( StatusBarNotification n, @NonNull DismissedByUserStats stats, int reason )274     public void performRemoveNotification(
275             StatusBarNotification n,
276             @NonNull DismissedByUserStats stats,
277             int reason
278     ) {
279         removeNotificationInternal(
280                 n.getKey(),
281                 null,
282                 stats.notificationVisibility,
283                 false /* forceRemove */,
284                 stats,
285                 reason);
286     }
287 
obtainVisibility(String key)288     private NotificationVisibility obtainVisibility(String key) {
289         NotificationEntry e = mActiveNotifications.get(key);
290         final int rank;
291         if (e != null) {
292             rank = e.getRanking().getRank();
293         } else {
294             rank = 0;
295         }
296 
297         final int count = mActiveNotifications.size();
298         NotificationVisibility.NotificationLocation location =
299                 NotificationLogger.getNotificationLocation(getActiveNotificationUnfiltered(key));
300         return NotificationVisibility.obtain(key, rank, count, true, location);
301     }
302 
abortExistingInflation(String key, String reason)303     private void abortExistingInflation(String key, String reason) {
304         if (mPendingNotifications.containsKey(key)) {
305             NotificationEntry entry = mPendingNotifications.get(key);
306             entry.abortTask();
307             mPendingNotifications.remove(key);
308             mLogger.logInflationAborted(key, "pending", reason);
309         }
310         NotificationEntry addedEntry = getActiveNotificationUnfiltered(key);
311         if (addedEntry != null) {
312             addedEntry.abortTask();
313             mLogger.logInflationAborted(key, "active", reason);
314         }
315     }
316 
317     /**
318      * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
319      * about the failure.
320      *
321      * WARNING: this will call back into us.  Don't hold any locks.
322      */
handleInflationException(StatusBarNotification n, Exception e)323     private void handleInflationException(StatusBarNotification n, Exception e) {
324         removeNotificationInternal(
325                 n.getKey(),
326                 null,
327                 null,
328                 true /* forceRemove */,
329                 null /* dismissedByUserStats */,
330                 REASON_ERROR);
331         for (NotificationEntryListener listener : mNotificationEntryListeners) {
332             listener.onInflationError(n, e);
333         }
334     }
335 
336     private final InflationCallback mInflationCallback = new InflationCallback() {
337         @Override
338         public void handleInflationException(NotificationEntry entry, Exception e) {
339             NotificationEntryManager.this.handleInflationException(entry.getSbn(), e);
340         }
341 
342         @Override
343         public void onAsyncInflationFinished(NotificationEntry entry) {
344             mPendingNotifications.remove(entry.getKey());
345             // If there was an async task started after the removal, we don't want to add it back to
346             // the list, otherwise we might get leaks.
347             if (!entry.isRowRemoved()) {
348                 boolean isNew = getActiveNotificationUnfiltered(entry.getKey()) == null;
349                 mLogger.logNotifInflated(entry.getKey(), isNew);
350                 if (isNew) {
351                     for (NotificationEntryListener listener : mNotificationEntryListeners) {
352                         listener.onEntryInflated(entry);
353                     }
354                     addActiveNotification(entry);
355                     updateNotifications("onAsyncInflationFinished");
356                     for (NotificationEntryListener listener : mNotificationEntryListeners) {
357                         listener.onNotificationAdded(entry);
358                     }
359                 } else {
360                     for (NotificationEntryListener listener : mNotificationEntryListeners) {
361                         listener.onEntryReinflated(entry);
362                     }
363                 }
364             }
365         }
366     };
367 
368     private final NotificationHandler mNotifListener = new NotificationHandler() {
369         @Override
370         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
371             final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey());
372             if (isUpdateToInflatedNotif) {
373                 updateNotification(sbn, rankingMap);
374             } else {
375                 addNotification(sbn, rankingMap);
376             }
377         }
378 
379         @Override
380         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
381             removeNotification(sbn.getKey(), rankingMap, UNDEFINED_DISMISS_REASON);
382         }
383 
384         @Override
385         public void onNotificationRemoved(
386                 StatusBarNotification sbn,
387                 RankingMap rankingMap,
388                 int reason) {
389             removeNotification(sbn.getKey(), rankingMap, reason);
390         }
391 
392         @Override
393         public void onNotificationRankingUpdate(RankingMap rankingMap) {
394             updateNotificationRanking(rankingMap);
395         }
396 
397         @Override
398         public void onNotificationsInitialized() {
399         }
400     };
401 
402     /**
403      * Equivalent to the old NotificationData#add
404      * @param entry - an entry which is prepared for display
405      */
addActiveNotification(NotificationEntry entry)406     private void addActiveNotification(NotificationEntry entry) {
407         Assert.isMainThread();
408 
409         mActiveNotifications.put(entry.getKey(), entry);
410         mGroupManager.onEntryAdded(entry);
411         updateRankingAndSort(mRanker.getRankingMap(), "addEntryInternalInternal");
412     }
413 
414     /**
415      * Available so that tests can directly manipulate the list of active notifications easily
416      *
417      * @param entry the entry to add directly to the visible notification map
418      */
419     @VisibleForTesting
addActiveNotificationForTest(NotificationEntry entry)420     public void addActiveNotificationForTest(NotificationEntry entry) {
421         mActiveNotifications.put(entry.getKey(), entry);
422         mGroupManager.onEntryAdded(entry);
423 
424         reapplyFilterAndSort("addVisibleNotification");
425     }
426 
427     @VisibleForTesting
removeNotification(String key, RankingMap ranking, int reason)428     protected void removeNotification(String key, RankingMap ranking, int reason) {
429         removeNotificationInternal(
430                 key,
431                 ranking,
432                 obtainVisibility(key),
433                 false /* forceRemove */,
434                 null /* dismissedByUserStats */,
435                 reason);
436     }
437 
438     /**
439      * Internally remove a notification because system server has reported the notification
440      * should be removed OR the user has manually dismissed the notification
441      * @param dismissedByUserStats non-null if the user manually dismissed the notification
442      */
removeNotificationInternal( String key, @Nullable RankingMap ranking, @Nullable NotificationVisibility visibility, boolean forceRemove, DismissedByUserStats dismissedByUserStats, int reason)443     private void removeNotificationInternal(
444             String key,
445             @Nullable RankingMap ranking,
446             @Nullable NotificationVisibility visibility,
447             boolean forceRemove,
448             DismissedByUserStats dismissedByUserStats,
449             int reason) {
450 
451         final NotificationEntry entry = getActiveNotificationUnfiltered(key);
452 
453         for (NotificationRemoveInterceptor interceptor : mRemoveInterceptors) {
454             if (interceptor.onNotificationRemoveRequested(key, entry, reason)) {
455                 // Remove intercepted; log and skip
456                 mLogger.logRemovalIntercepted(key);
457                 return;
458             }
459         }
460 
461         boolean lifetimeExtended = false;
462 
463         // Notification was canceled before it got inflated
464         if (entry == null) {
465             NotificationEntry pendingEntry = mPendingNotifications.get(key);
466             if (pendingEntry != null) {
467                 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
468                     if (extender.shouldExtendLifetimeForPendingNotification(pendingEntry)) {
469                         extendLifetime(pendingEntry, extender);
470                         lifetimeExtended = true;
471                         mLogger.logLifetimeExtended(key, extender.getClass().getName(), "pending");
472                     }
473                 }
474                 if (!lifetimeExtended) {
475                     // At this point, we are guaranteed the notification will be removed
476                     abortExistingInflation(key, "removeNotification");
477                     // Fix for b/201097913: NotifCollectionListener#onEntryRemoved specifies that
478                     //   #onEntryRemoved should be called when a notification is cancelled,
479                     //   regardless of whether the notification was pending or active.
480                     // Note that mNotificationEntryListeners are NOT notified of #onEntryRemoved
481                     //   because for that interface, #onEntryRemoved should only be called for
482                     //   active entries, NOT pending ones.
483                     for (NotifCollectionListener listener : mNotifCollectionListeners) {
484                         listener.onEntryRemoved(pendingEntry, REASON_UNKNOWN);
485                     }
486                     for (NotifCollectionListener listener : mNotifCollectionListeners) {
487                         listener.onEntryCleanUp(pendingEntry);
488                     }
489                     mAllNotifications.remove(pendingEntry);
490                     mLeakDetector.trackGarbage(pendingEntry);
491                 }
492             }
493         } else {
494             // If a manager needs to keep the notification around for whatever reason, we
495             // keep the notification
496             boolean entryDismissed = entry.isRowDismissed();
497             if (!forceRemove && !entryDismissed) {
498                 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
499                     if (extender.shouldExtendLifetime(entry)) {
500                         mLatestRankingMap = ranking;
501                         extendLifetime(entry, extender);
502                         lifetimeExtended = true;
503                         mLogger.logLifetimeExtended(key, extender.getClass().getName(), "active");
504                         break;
505                     }
506                 }
507             }
508 
509             if (!lifetimeExtended) {
510                 // At this point, we are guaranteed the notification will be removed
511                 abortExistingInflation(key, "removeNotification");
512                 mAllNotifications.remove(entry);
513 
514                 // Ensure any managers keeping the lifetime extended stop managing the entry
515                 cancelLifetimeExtension(entry);
516 
517                 if (entry.rowExists()) {
518                     entry.removeRow();
519                 }
520 
521                 // Let's remove the children if this was a summary
522                 handleGroupSummaryRemoved(key);
523                 removeVisibleNotification(key);
524                 updateNotifications("removeNotificationInternal");
525                 final boolean removedByUser = dismissedByUserStats != null;
526 
527                 mLogger.logNotifRemoved(entry.getKey(), removedByUser);
528                 if (removedByUser && visibility != null) {
529                     sendNotificationRemovalToServer(entry.getSbn(), dismissedByUserStats);
530                 }
531                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
532                     listener.onEntryRemoved(entry, visibility, removedByUser, reason);
533                 }
534                 for (NotifCollectionListener listener : mNotifCollectionListeners) {
535                     // NEM doesn't have a good knowledge of reasons so defaulting to unknown.
536                     listener.onEntryRemoved(entry, REASON_UNKNOWN);
537                 }
538                 for (NotifCollectionListener listener : mNotifCollectionListeners) {
539                     listener.onEntryCleanUp(entry);
540                 }
541                 mLeakDetector.trackGarbage(entry);
542             }
543         }
544     }
545 
sendNotificationRemovalToServer( StatusBarNotification notification, DismissedByUserStats dismissedByUserStats)546     private void sendNotificationRemovalToServer(
547             StatusBarNotification notification,
548             DismissedByUserStats dismissedByUserStats) {
549         try {
550             mStatusBarService.onNotificationClear(
551                     notification.getPackageName(),
552                     notification.getUser().getIdentifier(),
553                     notification.getKey(),
554                     dismissedByUserStats.dismissalSurface,
555                     dismissedByUserStats.dismissalSentiment,
556                     dismissedByUserStats.notificationVisibility);
557         } catch (RemoteException ex) {
558             // system process is dead if we're here.
559         }
560     }
561 
562     /**
563      * Ensures that the group children are cancelled immediately when the group summary is cancelled
564      * instead of waiting for the notification manager to send all cancels. Otherwise this could
565      * lead to flickers.
566      *
567      * This also ensures that the animation looks nice and only consists of a single disappear
568      * animation instead of multiple.
569      *  @param key the key of the notification was removed
570      *
571      */
handleGroupSummaryRemoved(String key)572     private void handleGroupSummaryRemoved(String key) {
573         NotificationEntry entry = getActiveNotificationUnfiltered(key);
574         if (entry != null && entry.rowExists() && entry.isSummaryWithChildren()) {
575             if (entry.getSbn().getOverrideGroupKey() != null && !entry.isRowDismissed()) {
576                 // We don't want to remove children for autobundled notifications as they are not
577                 // always cancelled. We only remove them if they were dismissed by the user.
578                 return;
579             }
580             List<NotificationEntry> childEntries = entry.getAttachedNotifChildren();
581             if (childEntries == null) {
582                 return;
583             }
584             for (int i = 0; i < childEntries.size(); i++) {
585                 NotificationEntry childEntry = childEntries.get(i);
586                 boolean isForeground = (entry.getSbn().getNotification().flags
587                         & Notification.FLAG_FOREGROUND_SERVICE) != 0;
588                 boolean keepForReply =
589                         mRemoteInputManagerLazy.get().shouldKeepForRemoteInputHistory(childEntry)
590                         || mRemoteInputManagerLazy.get().shouldKeepForSmartReplyHistory(childEntry);
591                 if (isForeground || keepForReply) {
592                     // the child is a foreground service notification which we can't remove or it's
593                     // a child we're keeping around for reply!
594                     continue;
595                 }
596                 childEntry.setKeepInParent(true);
597                 // we need to set this state earlier as otherwise we might generate some weird
598                 // animations
599                 childEntry.removeRow();
600             }
601         }
602     }
603 
addNotificationInternal( StatusBarNotification notification, RankingMap rankingMap)604     private void addNotificationInternal(
605             StatusBarNotification notification,
606             RankingMap rankingMap) throws InflationException {
607         String key = notification.getKey();
608         if (DEBUG) {
609             Log.d(TAG, "addNotification key=" + key);
610         }
611 
612         updateRankingAndSort(rankingMap, "addNotificationInternal");
613 
614         Ranking ranking = new Ranking();
615         rankingMap.getRanking(key, ranking);
616 
617         NotificationEntry entry = mPendingNotifications.get(key);
618         if (entry != null) {
619             entry.setSbn(notification);
620             entry.setRanking(ranking);
621         } else {
622             entry = new NotificationEntry(
623                     notification,
624                     ranking,
625                     mFgsFeatureController.isForegroundServiceDismissalEnabled(),
626                     SystemClock.uptimeMillis());
627             mAllNotifications.add(entry);
628             mLeakDetector.trackInstance(entry);
629 
630             for (NotifCollectionListener listener : mNotifCollectionListeners) {
631                 listener.onEntryInit(entry);
632             }
633         }
634 
635         for (NotifCollectionListener listener : mNotifCollectionListeners) {
636             listener.onEntryBind(entry, notification);
637         }
638 
639         // Construct the expanded view.
640         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
641             mNotificationRowBinderLazy.get().inflateViews(entry, mInflationCallback);
642         }
643 
644         mPendingNotifications.put(key, entry);
645         mLogger.logNotifAdded(entry.getKey());
646         for (NotificationEntryListener listener : mNotificationEntryListeners) {
647             listener.onPendingEntryAdded(entry);
648         }
649         for (NotifCollectionListener listener : mNotifCollectionListeners) {
650             listener.onEntryAdded(entry);
651         }
652         for (NotifCollectionListener listener : mNotifCollectionListeners) {
653             listener.onRankingApplied();
654         }
655     }
656 
addNotification(StatusBarNotification notification, RankingMap ranking)657     public void addNotification(StatusBarNotification notification, RankingMap ranking) {
658         try {
659             addNotificationInternal(notification, ranking);
660         } catch (InflationException e) {
661             handleInflationException(notification, e);
662         }
663     }
664 
updateNotificationInternal(StatusBarNotification notification, RankingMap ranking)665     private void updateNotificationInternal(StatusBarNotification notification,
666             RankingMap ranking) throws InflationException {
667         if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
668 
669         final String key = notification.getKey();
670         abortExistingInflation(key, "updateNotification");
671         final NotificationEntry entry = getActiveNotificationUnfiltered(key);
672         if (entry == null) {
673             return;
674         }
675 
676         // Notification is updated so it is essentially re-added and thus alive again.  Don't need
677         // to keep its lifetime extended.
678         cancelLifetimeExtension(entry);
679 
680         updateRankingAndSort(ranking, "updateNotificationInternal");
681         StatusBarNotification oldSbn = entry.getSbn();
682         entry.setSbn(notification);
683         for (NotifCollectionListener listener : mNotifCollectionListeners) {
684             listener.onEntryBind(entry, notification);
685         }
686         mGroupManager.onEntryUpdated(entry, oldSbn);
687 
688         mLogger.logNotifUpdated(entry.getKey());
689         for (NotificationEntryListener listener : mNotificationEntryListeners) {
690             listener.onPreEntryUpdated(entry);
691         }
692         final boolean fromSystem = ranking != null;
693         for (NotifCollectionListener listener : mNotifCollectionListeners) {
694             listener.onEntryUpdated(entry, fromSystem);
695         }
696 
697         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
698             mNotificationRowBinderLazy.get().inflateViews(entry, mInflationCallback);
699         }
700 
701         updateNotifications("updateNotificationInternal");
702 
703         for (NotificationEntryListener listener : mNotificationEntryListeners) {
704             listener.onPostEntryUpdated(entry);
705         }
706         for (NotifCollectionListener listener : mNotifCollectionListeners) {
707             listener.onRankingApplied();
708         }
709     }
710 
updateNotification(StatusBarNotification notification, RankingMap ranking)711     public void updateNotification(StatusBarNotification notification, RankingMap ranking) {
712         try {
713             updateNotificationInternal(notification, ranking);
714         } catch (InflationException e) {
715             handleInflationException(notification, e);
716         }
717     }
718 
719     /**
720      * Update the notifications
721      * @param reason why the notifications are updating
722      */
updateNotifications(String reason)723     public void updateNotifications(String reason) {
724         reapplyFilterAndSort(reason);
725         if (mPresenter != null && !mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
726             mPresenter.updateNotificationViews(reason);
727         }
728     }
729 
updateNotificationRanking(RankingMap rankingMap)730     public void updateNotificationRanking(RankingMap rankingMap) {
731         List<NotificationEntry> entries = new ArrayList<>();
732         entries.addAll(getVisibleNotifications());
733         entries.addAll(mPendingNotifications.values());
734 
735         // Has a copy of the current UI adjustments.
736         ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>();
737         ArrayMap<String, Integer> oldImportances = new ArrayMap<>();
738         for (NotificationEntry entry : entries) {
739             NotificationUiAdjustment adjustment =
740                     NotificationUiAdjustment.extractFromNotificationEntry(entry);
741             oldAdjustments.put(entry.getKey(), adjustment);
742             oldImportances.put(entry.getKey(), entry.getImportance());
743         }
744 
745         // Populate notification entries from the new rankings.
746         updateRankingAndSort(rankingMap, "updateNotificationRanking");
747         updateRankingOfPendingNotifications(rankingMap);
748 
749         // By comparing the old and new UI adjustments, reinflate the view accordingly.
750         for (NotificationEntry entry : entries) {
751             mNotificationRowBinderLazy.get()
752                     .onNotificationRankingUpdated(
753                             entry,
754                             oldImportances.get(entry.getKey()),
755                             oldAdjustments.get(entry.getKey()),
756                             NotificationUiAdjustment.extractFromNotificationEntry(entry),
757                             mInflationCallback);
758         }
759 
760         updateNotifications("updateNotificationRanking");
761 
762         for (NotificationEntryListener listener : mNotificationEntryListeners) {
763             listener.onNotificationRankingUpdated(rankingMap);
764         }
765         for (NotifCollectionListener listener : mNotifCollectionListeners) {
766             listener.onRankingUpdate(rankingMap);
767         }
768         for (NotifCollectionListener listener : mNotifCollectionListeners) {
769             listener.onRankingApplied();
770         }
771     }
772 
updateRankingOfPendingNotifications(@ullable RankingMap rankingMap)773     private void updateRankingOfPendingNotifications(@Nullable RankingMap rankingMap) {
774         if (rankingMap == null) {
775             return;
776         }
777         for (NotificationEntry pendingNotification : mPendingNotifications.values()) {
778             Ranking ranking = new Ranking();
779             if (rankingMap.getRanking(pendingNotification.getKey(), ranking)) {
780                 pendingNotification.setRanking(ranking);
781             }
782         }
783     }
784 
785     /**
786      * @return An iterator for all "pending" notifications. Pending notifications are newly-posted
787      * notifications whose views have not yet been inflated. In general, the system pretends like
788      * these don't exist, although there are a couple exceptions.
789      */
getPendingNotificationsIterator()790     public Iterable<NotificationEntry> getPendingNotificationsIterator() {
791         return mPendingNotifications.values();
792     }
793 
794     /**
795      * Use this method to retrieve a notification entry that has been prepared for presentation.
796      * Note that the notification may be filtered out and never shown to the user.
797      *
798      * @see #getVisibleNotifications() for the currently sorted and filtered list
799      *
800      * @return a {@link NotificationEntry} if it has been prepared, else null
801      */
getActiveNotificationUnfiltered(String key)802     public NotificationEntry getActiveNotificationUnfiltered(String key) {
803         return mActiveNotifications.get(key);
804     }
805 
806     /**
807      * Gets the pending or visible notification entry with the given key. Returns null if
808      * notification doesn't exist.
809      */
getPendingOrActiveNotif(String key)810     public NotificationEntry getPendingOrActiveNotif(String key) {
811         if (mPendingNotifications.containsKey(key)) {
812             return mPendingNotifications.get(key);
813         } else {
814             return mActiveNotifications.get(key);
815         }
816     }
817 
extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender)818     private void extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender) {
819         NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry);
820         if (activeExtender != null && activeExtender != extender) {
821             activeExtender.setShouldManageLifetime(entry, false);
822         }
823         mRetainedNotifications.put(entry, extender);
824         extender.setShouldManageLifetime(entry, true);
825     }
826 
cancelLifetimeExtension(NotificationEntry entry)827     private void cancelLifetimeExtension(NotificationEntry entry) {
828         NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry);
829         if (activeExtender != null) {
830             activeExtender.setShouldManageLifetime(entry, false);
831         }
832     }
833 
834     /*
835      * -----
836      * Annexed from NotificationData below:
837      * Some of these methods may be redundant but require some reworking to remove. For now
838      * we'll try to keep the behavior the same and can simplify these interfaces in another pass
839      */
840 
841     /** Internalization of NotificationData#remove */
removeVisibleNotification(String key)842     private void removeVisibleNotification(String key) {
843         // no need to synchronize if we're on the main thread dawg
844         Assert.isMainThread();
845 
846         NotificationEntry removed = mActiveNotifications.remove(key);
847 
848         if (removed == null) return;
849         mGroupManager.onEntryRemoved(removed);
850     }
851 
852     /** @return list of active notifications filtered for the current user */
getActiveNotificationsForCurrentUser()853     public List<NotificationEntry> getActiveNotificationsForCurrentUser() {
854         Assert.isMainThread();
855         ArrayList<NotificationEntry> filtered = new ArrayList<>();
856 
857         final int len = mActiveNotifications.size();
858         for (int i = 0; i < len; i++) {
859             NotificationEntry entry = mActiveNotifications.valueAt(i);
860             if (!mRanker.isNotificationForCurrentProfiles(entry)) {
861                 continue;
862             }
863             filtered.add(entry);
864         }
865 
866         return filtered;
867     }
868 
869     //TODO: Get rid of this in favor of NotificationUpdateHandler#updateNotificationRanking
870     /**
871      * @param rankingMap the {@link RankingMap} to apply to the current notification list
872      * @param reason the reason for calling this method, which will be logged
873      */
updateRanking(RankingMap rankingMap, String reason)874     public void updateRanking(RankingMap rankingMap, String reason) {
875         updateRankingAndSort(rankingMap, reason);
876         for (NotifCollectionListener listener : mNotifCollectionListeners) {
877             listener.onRankingApplied();
878         }
879     }
880 
881     /** Resorts / filters the current notification set with the current RankingMap */
reapplyFilterAndSort(String reason)882     public void reapplyFilterAndSort(String reason) {
883         updateRankingAndSort(mRanker.getRankingMap(), reason);
884     }
885 
886     /** Calls to NotificationRankingManager and updates mSortedAndFiltered */
updateRankingAndSort(@onNull RankingMap rankingMap, String reason)887     private void updateRankingAndSort(@NonNull RankingMap rankingMap, String reason) {
888         mSortedAndFiltered.clear();
889         mSortedAndFiltered.addAll(mRanker.updateRanking(
890                 rankingMap, mActiveNotifications.values(), reason));
891     }
892 
893     /** dump the current active notification list. Called from StatusBar */
dump(PrintWriter pw, String indent)894     public void dump(PrintWriter pw, String indent) {
895         pw.println("NotificationEntryManager");
896         int filteredLen = mSortedAndFiltered.size();
897         pw.print(indent);
898         pw.println("active notifications: " + filteredLen);
899         int active;
900         for (active = 0; active < filteredLen; active++) {
901             NotificationEntry e = mSortedAndFiltered.get(active);
902             dumpEntry(pw, indent, active, e);
903         }
904         synchronized (mActiveNotifications) {
905             int totalLen = mActiveNotifications.size();
906             pw.print(indent);
907             pw.println("inactive notifications: " + (totalLen - active));
908             int inactiveCount = 0;
909             for (int i = 0; i < totalLen; i++) {
910                 NotificationEntry entry = mActiveNotifications.valueAt(i);
911                 if (!mSortedAndFiltered.contains(entry)) {
912                     dumpEntry(pw, indent, inactiveCount, entry);
913                     inactiveCount++;
914                 }
915             }
916         }
917     }
918 
dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e)919     private void dumpEntry(PrintWriter pw, String indent, int i, NotificationEntry e) {
920         pw.print(indent);
921         pw.println("  [" + i + "] key=" + e.getKey() + " icon=" + e.getIcons().getStatusBarIcon());
922         StatusBarNotification n = e.getSbn();
923         pw.print(indent);
924         pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " importance="
925                 + e.getRanking().getImportance());
926         pw.print(indent);
927         pw.println("      notification=" + n.getNotification());
928     }
929 
930     /**
931      * This is the answer to the question "what notifications should the user be seeing right now?"
932      * These are sorted and filtered, and directly inform the notification shade what to show
933      *
934      * @return A read-only list of the currently active notifications
935      */
getVisibleNotifications()936     public List<NotificationEntry> getVisibleNotifications() {
937         return mReadOnlyNotifications;
938     }
939 
940     /**
941      * Returns a collections containing ALL notifications we know about, including ones that are
942      * hidden or for other users. See {@link CommonNotifCollection#getAllNotifs()}.
943      */
944     @Override
getAllNotifs()945     public Collection<NotificationEntry> getAllNotifs() {
946         return mReadOnlyAllNotifications;
947     }
948 
949     /** @return A count of the active notifications */
getActiveNotificationsCount()950     public int getActiveNotificationsCount() {
951         return mReadOnlyNotifications.size();
952     }
953 
954     /**
955      * @return {@code true} if there is at least one notification that should be visible right now
956      */
hasActiveNotifications()957     public boolean hasActiveNotifications() {
958         return mReadOnlyNotifications.size() != 0;
959     }
960 
961     @Override
addCollectionListener(NotifCollectionListener listener)962     public void addCollectionListener(NotifCollectionListener listener) {
963         mNotifCollectionListeners.add(listener);
964     }
965 
966     /*
967      * End annexation
968      * -----
969      */
970 
971 
972     /**
973      * Provides access to keyguard state and user settings dependent data.
974      */
975     public interface KeyguardEnvironment {
976         /** true if the device is provisioned (should always be true in practice) */
isDeviceProvisioned()977         boolean isDeviceProvisioned();
978         /** true if the notification is for the current profiles */
isNotificationForCurrentProfiles(StatusBarNotification sbn)979         boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
980     }
981 
982     private static final String TAG = "NotificationEntryMgr";
983     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
984 
985     /**
986      * Used when a notification is removed and it doesn't have a reason that maps to one of the
987      * reasons defined in NotificationListenerService
988      * (e.g. {@link NotificationListenerService#REASON_CANCEL})
989      */
990     public static final int UNDEFINED_DISMISS_REASON = 0;
991 }
992