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