• 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 android.annotation.Nullable;
22 import android.app.Notification;
23 import android.content.Context;
24 import android.service.notification.NotificationListenerService;
25 import android.service.notification.StatusBarNotification;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.statusbar.NotificationVisibility;
31 import com.android.systemui.Dependency;
32 import com.android.systemui.Dumpable;
33 import com.android.systemui.statusbar.NotificationLifetimeExtender;
34 import com.android.systemui.statusbar.NotificationPresenter;
35 import com.android.systemui.statusbar.NotificationRemoteInputManager;
36 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
37 import com.android.systemui.statusbar.NotificationUiAdjustment;
38 import com.android.systemui.statusbar.NotificationUpdateHandler;
39 import com.android.systemui.statusbar.notification.collection.NotificationData;
40 import com.android.systemui.statusbar.notification.collection.NotificationData.KeyguardEnvironment;
41 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
42 import com.android.systemui.statusbar.notification.collection.NotificationRowBinder;
43 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
44 import com.android.systemui.statusbar.notification.row.NotificationContentInflater;
45 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
46 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
47 import com.android.systemui.statusbar.policy.HeadsUpManager;
48 import com.android.systemui.util.leak.LeakDetector;
49 
50 import java.io.FileDescriptor;
51 import java.io.PrintWriter;
52 import java.util.ArrayList;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Map;
56 
57 /**
58  * NotificationEntryManager is responsible for the adding, removing, and updating of notifications.
59  * It also handles tasks such as their inflation and their interaction with other
60  * Notification.*Manager objects.
61  */
62 public class NotificationEntryManager implements
63         Dumpable,
64         NotificationContentInflater.InflationCallback,
65         NotificationUpdateHandler,
66         VisualStabilityManager.Callback {
67     private static final String TAG = "NotificationEntryMgr";
68     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
69 
70     /**
71      * Used when a notification is removed and it doesn't have a reason that maps to one of the
72      * reasons defined in NotificationListenerService
73      * (e.g. {@link NotificationListenerService.REASON_CANCEL})
74      */
75     public static final int UNDEFINED_DISMISS_REASON = 0;
76 
77     @VisibleForTesting
78     protected final HashMap<String, NotificationEntry> mPendingNotifications = new HashMap<>();
79 
80     private final Map<NotificationEntry, NotificationLifetimeExtender> mRetainedNotifications =
81             new ArrayMap<>();
82 
83     // Lazily retrieved dependencies
84     private NotificationRemoteInputManager mRemoteInputManager;
85     private NotificationRowBinder mNotificationRowBinder;
86 
87     private NotificationPresenter mPresenter;
88     @VisibleForTesting
89     protected NotificationData mNotificationData;
90 
91     @VisibleForTesting
92     final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
93             = new ArrayList<>();
94     private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>();
95     private NotificationRemoveInterceptor mRemoveInterceptor;
96 
97     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)98     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
99         pw.println("NotificationEntryManager state:");
100         pw.print("  mPendingNotifications=");
101         if (mPendingNotifications.size() == 0) {
102             pw.println("null");
103         } else {
104             for (NotificationEntry entry : mPendingNotifications.values()) {
105                 pw.println(entry.notification);
106             }
107         }
108         pw.println("  Lifetime-extended notifications:");
109         if (mRetainedNotifications.isEmpty()) {
110             pw.println("    None");
111         } else {
112             for (Map.Entry<NotificationEntry, NotificationLifetimeExtender> entry
113                     : mRetainedNotifications.entrySet()) {
114                 pw.println("    " + entry.getKey().notification + " retained by "
115                         + entry.getValue().getClass().getName());
116             }
117         }
118     }
119 
NotificationEntryManager(Context context)120     public NotificationEntryManager(Context context) {
121         mNotificationData = new NotificationData();
122     }
123 
124     /** Adds a {@link NotificationEntryListener}. */
addNotificationEntryListener(NotificationEntryListener listener)125     public void addNotificationEntryListener(NotificationEntryListener listener) {
126         mNotificationEntryListeners.add(listener);
127     }
128 
129     /** Sets the {@link NotificationRemoveInterceptor}. */
setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor)130     public void setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
131         mRemoveInterceptor = interceptor;
132     }
133 
134     /**
135      * Our dependencies can have cyclic references, so some need to be lazy
136      */
getRemoteInputManager()137     private NotificationRemoteInputManager getRemoteInputManager() {
138         if (mRemoteInputManager == null) {
139             mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
140         }
141         return mRemoteInputManager;
142     }
143 
setRowBinder(NotificationRowBinder notificationRowBinder)144     public void setRowBinder(NotificationRowBinder notificationRowBinder) {
145         mNotificationRowBinder = notificationRowBinder;
146     }
147 
setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, HeadsUpManager headsUpManager)148     public void setUpWithPresenter(NotificationPresenter presenter,
149             NotificationListContainer listContainer,
150             HeadsUpManager headsUpManager) {
151         mPresenter = presenter;
152         mNotificationData.setHeadsUpManager(headsUpManager);
153     }
154 
155     /** Adds multiple {@link NotificationLifetimeExtender}s. */
addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders)156     public void addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders) {
157         for (NotificationLifetimeExtender extender : extenders) {
158             addNotificationLifetimeExtender(extender);
159         }
160     }
161 
162     /** Adds a {@link NotificationLifetimeExtender}. */
addNotificationLifetimeExtender(NotificationLifetimeExtender extender)163     public void addNotificationLifetimeExtender(NotificationLifetimeExtender extender) {
164         mNotificationLifetimeExtenders.add(extender);
165         extender.setCallback(key -> removeNotification(key, null, UNDEFINED_DISMISS_REASON));
166     }
167 
getNotificationData()168     public NotificationData getNotificationData() {
169         return mNotificationData;
170     }
171 
172     @Override
onReorderingAllowed()173     public void onReorderingAllowed() {
174         updateNotifications();
175     }
176 
177     /**
178      * Requests a notification to be removed.
179      *
180      * @param n the notification to remove.
181      * @param reason why it is being removed e.g. {@link NotificationListenerService#REASON_CANCEL},
182      *               or 0 if unknown.
183      */
performRemoveNotification(StatusBarNotification n, int reason)184     public void performRemoveNotification(StatusBarNotification n, int reason) {
185         final NotificationVisibility nv = obtainVisibility(n.getKey());
186         removeNotificationInternal(
187                 n.getKey(), null, nv, false /* forceRemove */, true /* removedByUser */,
188                 reason);
189     }
190 
obtainVisibility(String key)191     private NotificationVisibility obtainVisibility(String key) {
192         final int rank = mNotificationData.getRank(key);
193         final int count = mNotificationData.getActiveNotifications().size();
194         NotificationVisibility.NotificationLocation location =
195                 NotificationLogger.getNotificationLocation(getNotificationData().get(key));
196         return NotificationVisibility.obtain(key, rank, count, true, location);
197     }
198 
abortExistingInflation(String key)199     private void abortExistingInflation(String key) {
200         if (mPendingNotifications.containsKey(key)) {
201             NotificationEntry entry = mPendingNotifications.get(key);
202             entry.abortTask();
203             mPendingNotifications.remove(key);
204         }
205         NotificationEntry addedEntry = mNotificationData.get(key);
206         if (addedEntry != null) {
207             addedEntry.abortTask();
208         }
209     }
210 
211     /**
212      * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
213      * about the failure.
214      *
215      * WARNING: this will call back into us.  Don't hold any locks.
216      */
217     @Override
handleInflationException(StatusBarNotification n, Exception e)218     public void handleInflationException(StatusBarNotification n, Exception e) {
219         removeNotificationInternal(
220                 n.getKey(), null, null, true /* forceRemove */, false /* removedByUser */,
221                 REASON_ERROR);
222         for (NotificationEntryListener listener : mNotificationEntryListeners) {
223             listener.onInflationError(n, e);
224         }
225     }
226 
227     @Override
onAsyncInflationFinished(NotificationEntry entry, @InflationFlag int inflatedFlags)228     public void onAsyncInflationFinished(NotificationEntry entry,
229             @InflationFlag int inflatedFlags) {
230         mPendingNotifications.remove(entry.key);
231         // If there was an async task started after the removal, we don't want to add it back to
232         // the list, otherwise we might get leaks.
233         if (!entry.isRowRemoved()) {
234             boolean isNew = mNotificationData.get(entry.key) == null;
235             if (isNew) {
236                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
237                     listener.onEntryInflated(entry, inflatedFlags);
238                 }
239                 mNotificationData.add(entry);
240                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
241                     listener.onBeforeNotificationAdded(entry);
242                 }
243                 updateNotifications();
244                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
245                     listener.onNotificationAdded(entry);
246                 }
247             } else {
248                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
249                     listener.onEntryReinflated(entry);
250                 }
251             }
252         }
253     }
254 
255     @Override
removeNotification(String key, NotificationListenerService.RankingMap ranking, int reason)256     public void removeNotification(String key, NotificationListenerService.RankingMap ranking,
257             int reason) {
258         removeNotificationInternal(key, ranking, obtainVisibility(key), false /* forceRemove */,
259                 false /* removedByUser */, reason);
260     }
261 
removeNotificationInternal( String key, @Nullable NotificationListenerService.RankingMap ranking, @Nullable NotificationVisibility visibility, boolean forceRemove, boolean removedByUser, int reason)262     private void removeNotificationInternal(
263             String key,
264             @Nullable NotificationListenerService.RankingMap ranking,
265             @Nullable NotificationVisibility visibility,
266             boolean forceRemove,
267             boolean removedByUser,
268             int reason) {
269 
270         if (mRemoveInterceptor != null
271                 && mRemoveInterceptor.onNotificationRemoveRequested(key, reason)) {
272             // Remove intercepted; skip
273             return;
274         }
275 
276         final NotificationEntry entry = mNotificationData.get(key);
277         boolean lifetimeExtended = false;
278 
279         // Notification was canceled before it got inflated
280         if (entry == null) {
281             NotificationEntry pendingEntry = mPendingNotifications.get(key);
282             if (pendingEntry != null) {
283                 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
284                     if (extender.shouldExtendLifetimeForPendingNotification(pendingEntry)) {
285                         extendLifetime(pendingEntry, extender);
286                         lifetimeExtended = true;
287                     }
288                 }
289             }
290         }
291 
292         if (!lifetimeExtended) {
293             abortExistingInflation(key);
294         }
295 
296         if (entry != null) {
297             // If a manager needs to keep the notification around for whatever reason, we
298             // keep the notification
299             boolean entryDismissed = entry.isRowDismissed();
300             if (!forceRemove && !entryDismissed) {
301                 for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
302                     if (extender.shouldExtendLifetime(entry)) {
303                         extendLifetime(entry, extender);
304                         lifetimeExtended = true;
305                         break;
306                     }
307                 }
308             }
309 
310             if (!lifetimeExtended) {
311                 // At this point, we are guaranteed the notification will be removed
312 
313                 // Ensure any managers keeping the lifetime extended stop managing the entry
314                 cancelLifetimeExtension(entry);
315 
316                 if (entry.rowExists()) {
317                     entry.removeRow();
318                 }
319 
320                 // Let's remove the children if this was a summary
321                 handleGroupSummaryRemoved(key);
322 
323                 mNotificationData.remove(key, ranking);
324                 updateNotifications();
325                 Dependency.get(LeakDetector.class).trackGarbage(entry);
326                 removedByUser |= entryDismissed;
327 
328                 for (NotificationEntryListener listener : mNotificationEntryListeners) {
329                     listener.onEntryRemoved(entry, visibility, removedByUser);
330                 }
331             }
332         }
333     }
334 
335     /**
336      * Ensures that the group children are cancelled immediately when the group summary is cancelled
337      * instead of waiting for the notification manager to send all cancels. Otherwise this could
338      * lead to flickers.
339      *
340      * This also ensures that the animation looks nice and only consists of a single disappear
341      * animation instead of multiple.
342      *  @param key the key of the notification was removed
343      *
344      */
handleGroupSummaryRemoved(String key)345     private void handleGroupSummaryRemoved(String key) {
346         NotificationEntry entry = mNotificationData.get(key);
347         if (entry != null && entry.rowExists() && entry.isSummaryWithChildren()) {
348             if (entry.notification.getOverrideGroupKey() != null && !entry.isRowDismissed()) {
349                 // We don't want to remove children for autobundled notifications as they are not
350                 // always cancelled. We only remove them if they were dismissed by the user.
351                 return;
352             }
353             List<NotificationEntry> childEntries = entry.getChildren();
354             if (childEntries == null) {
355                 return;
356             }
357             for (int i = 0; i < childEntries.size(); i++) {
358                 NotificationEntry childEntry = childEntries.get(i);
359                 boolean isForeground = (entry.notification.getNotification().flags
360                         & Notification.FLAG_FOREGROUND_SERVICE) != 0;
361                 boolean keepForReply =
362                         getRemoteInputManager().shouldKeepForRemoteInputHistory(childEntry)
363                         || getRemoteInputManager().shouldKeepForSmartReplyHistory(childEntry);
364                 if (isForeground || keepForReply) {
365                     // the child is a foreground service notification which we can't remove or it's
366                     // a child we're keeping around for reply!
367                     continue;
368                 }
369                 childEntry.setKeepInParent(true);
370                 // we need to set this state earlier as otherwise we might generate some weird
371                 // animations
372                 childEntry.removeRow();
373             }
374         }
375     }
376 
addNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap rankingMap)377     private void addNotificationInternal(StatusBarNotification notification,
378             NotificationListenerService.RankingMap rankingMap) throws InflationException {
379         String key = notification.getKey();
380         if (DEBUG) {
381             Log.d(TAG, "addNotification key=" + key);
382         }
383 
384         mNotificationData.updateRanking(rankingMap);
385         NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
386         rankingMap.getRanking(key, ranking);
387 
388         NotificationEntry entry = new NotificationEntry(notification, ranking);
389 
390         Dependency.get(LeakDetector.class).trackInstance(entry);
391         // Construct the expanded view.
392         requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
393                 REASON_CANCEL));
394 
395         abortExistingInflation(key);
396 
397         mPendingNotifications.put(key, entry);
398         for (NotificationEntryListener listener : mNotificationEntryListeners) {
399             listener.onPendingEntryAdded(entry);
400         }
401     }
402 
403     @Override
addNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)404     public void addNotification(StatusBarNotification notification,
405             NotificationListenerService.RankingMap ranking) {
406         try {
407             addNotificationInternal(notification, ranking);
408         } catch (InflationException e) {
409             handleInflationException(notification, e);
410         }
411     }
412 
updateNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)413     private void updateNotificationInternal(StatusBarNotification notification,
414             NotificationListenerService.RankingMap ranking) throws InflationException {
415         if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
416 
417         final String key = notification.getKey();
418         abortExistingInflation(key);
419         NotificationEntry entry = mNotificationData.get(key);
420         if (entry == null) {
421             return;
422         }
423 
424         // Notification is updated so it is essentially re-added and thus alive again.  Don't need
425         // to keep its lifetime extended.
426         cancelLifetimeExtension(entry);
427 
428         mNotificationData.update(entry, ranking, notification);
429 
430         for (NotificationEntryListener listener : mNotificationEntryListeners) {
431             listener.onPreEntryUpdated(entry);
432         }
433 
434         requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
435                 REASON_CANCEL));
436         updateNotifications();
437 
438         if (DEBUG) {
439             // Is this for you?
440             boolean isForCurrentUser = Dependency.get(KeyguardEnvironment.class)
441                     .isNotificationForCurrentProfiles(notification);
442             Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
443         }
444 
445         for (NotificationEntryListener listener : mNotificationEntryListeners) {
446             listener.onPostEntryUpdated(entry);
447         }
448     }
449 
450     @Override
updateNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)451     public void updateNotification(StatusBarNotification notification,
452             NotificationListenerService.RankingMap ranking) {
453         try {
454             updateNotificationInternal(notification, ranking);
455         } catch (InflationException e) {
456             handleInflationException(notification, e);
457         }
458     }
459 
updateNotifications()460     public void updateNotifications() {
461         mNotificationData.filterAndSort();
462         if (mPresenter != null) {
463             mPresenter.updateNotificationViews();
464         }
465     }
466 
467     @Override
updateNotificationRanking(NotificationListenerService.RankingMap rankingMap)468     public void updateNotificationRanking(NotificationListenerService.RankingMap rankingMap) {
469         List<NotificationEntry> entries = new ArrayList<>();
470         entries.addAll(mNotificationData.getActiveNotifications());
471         entries.addAll(mPendingNotifications.values());
472 
473         // Has a copy of the current UI adjustments.
474         ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>();
475         ArrayMap<String, Integer> oldImportances = new ArrayMap<>();
476         for (NotificationEntry entry : entries) {
477             NotificationUiAdjustment adjustment =
478                     NotificationUiAdjustment.extractFromNotificationEntry(entry);
479             oldAdjustments.put(entry.key, adjustment);
480             oldImportances.put(entry.key, entry.importance);
481         }
482 
483         // Populate notification entries from the new rankings.
484         mNotificationData.updateRanking(rankingMap);
485         updateRankingOfPendingNotifications(rankingMap);
486 
487         // By comparing the old and new UI adjustments, reinflate the view accordingly.
488         for (NotificationEntry entry : entries) {
489             requireBinder().onNotificationRankingUpdated(
490                     entry,
491                     oldImportances.get(entry.key),
492                     oldAdjustments.get(entry.key),
493                     NotificationUiAdjustment.extractFromNotificationEntry(entry));
494         }
495 
496         updateNotifications();
497 
498         for (NotificationEntryListener listener : mNotificationEntryListeners) {
499             listener.onNotificationRankingUpdated(rankingMap);
500         }
501     }
502 
updateRankingOfPendingNotifications( @ullable NotificationListenerService.RankingMap rankingMap)503     private void updateRankingOfPendingNotifications(
504             @Nullable NotificationListenerService.RankingMap rankingMap) {
505         if (rankingMap == null) {
506             return;
507         }
508         NotificationListenerService.Ranking tmpRanking = new NotificationListenerService.Ranking();
509         for (NotificationEntry pendingNotification : mPendingNotifications.values()) {
510             rankingMap.getRanking(pendingNotification.key, tmpRanking);
511             pendingNotification.populateFromRanking(tmpRanking);
512         }
513     }
514 
515     /**
516      * @return An iterator for all "pending" notifications. Pending notifications are newly-posted
517      * notifications whose views have not yet been inflated. In general, the system pretends like
518      * these don't exist, although there are a couple exceptions.
519      */
getPendingNotificationsIterator()520     public Iterable<NotificationEntry> getPendingNotificationsIterator() {
521         return mPendingNotifications.values();
522     }
523 
extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender)524     private void extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender) {
525         NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry);
526         if (activeExtender != null && activeExtender != extender) {
527             activeExtender.setShouldManageLifetime(entry, false);
528         }
529         mRetainedNotifications.put(entry, extender);
530         extender.setShouldManageLifetime(entry, true);
531     }
532 
cancelLifetimeExtension(NotificationEntry entry)533     private void cancelLifetimeExtension(NotificationEntry entry) {
534         NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry);
535         if (activeExtender != null) {
536             activeExtender.setShouldManageLifetime(entry, false);
537         }
538     }
539 
requireBinder()540     private NotificationRowBinder requireBinder() {
541         if (mNotificationRowBinder == null) {
542             throw new RuntimeException("You must initialize NotificationEntryManager by calling"
543                     + "setRowBinder() before using.");
544         }
545         return mNotificationRowBinder;
546     }
547 }
548