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