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