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