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