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;
17 
18 import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
19 import static com.android.systemui.statusbar.NotificationRemoteInputManager
20         .FORCE_REMOTE_INPUT_HISTORY;
21 
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.content.Context;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.database.ContentObserver;
29 import android.os.Build;
30 import android.os.PowerManager;
31 import android.os.RemoteException;
32 import android.os.ServiceManager;
33 import android.os.SystemClock;
34 import android.os.UserHandle;
35 import android.provider.Settings;
36 import android.service.notification.NotificationListenerService;
37 import android.service.notification.NotificationStats;
38 import android.service.notification.StatusBarNotification;
39 import android.text.TextUtils;
40 import android.util.ArraySet;
41 import android.util.EventLog;
42 import android.util.Log;
43 import android.view.View;
44 import android.view.ViewGroup;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.logging.MetricsLogger;
48 import com.android.internal.statusbar.IStatusBarService;
49 import com.android.internal.statusbar.NotificationVisibility;
50 import com.android.internal.util.NotificationMessagingUtil;
51 import com.android.systemui.DejankUtils;
52 import com.android.systemui.Dependency;
53 import com.android.systemui.Dumpable;
54 import com.android.systemui.EventLogTags;
55 import com.android.systemui.ForegroundServiceController;
56 import com.android.systemui.R;
57 import com.android.systemui.UiOffloadThread;
58 import com.android.systemui.recents.misc.SystemServicesProxy;
59 import com.android.systemui.statusbar.notification.InflationException;
60 import com.android.systemui.statusbar.notification.NotificationInflater;
61 import com.android.systemui.statusbar.notification.RowInflaterTask;
62 import com.android.systemui.statusbar.notification.VisualStabilityManager;
63 import com.android.systemui.statusbar.phone.NotificationGroupManager;
64 import com.android.systemui.statusbar.phone.StatusBar;
65 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
66 import com.android.systemui.statusbar.policy.HeadsUpManager;
67 import com.android.systemui.util.leak.LeakDetector;
68 
69 import java.io.FileDescriptor;
70 import java.io.PrintWriter;
71 import java.util.ArrayList;
72 import java.util.HashMap;
73 import java.util.List;
74 
75 /**
76  * NotificationEntryManager is responsible for the adding, removing, and updating of notifications.
77  * It also handles tasks such as their inflation and their interaction with other
78  * Notification.*Manager objects.
79  */
80 public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,
81         ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,
82         VisualStabilityManager.Callback {
83     private static final String TAG = "NotificationEntryMgr";
84     protected static final boolean DEBUG = false;
85     protected static final boolean ENABLE_HEADS_UP = true;
86     protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
87 
88     protected final NotificationMessagingUtil mMessagingUtil;
89     protected final Context mContext;
90     protected final HashMap<String, NotificationData.Entry> mPendingNotifications = new HashMap<>();
91     protected final NotificationClicker mNotificationClicker = new NotificationClicker();
92     protected final ArraySet<NotificationData.Entry> mHeadsUpEntriesToRemoveOnSwitch =
93             new ArraySet<>();
94 
95     // Dependencies:
96     protected final NotificationLockscreenUserManager mLockscreenUserManager =
97             Dependency.get(NotificationLockscreenUserManager.class);
98     protected final NotificationGroupManager mGroupManager =
99             Dependency.get(NotificationGroupManager.class);
100     protected final NotificationGutsManager mGutsManager =
101             Dependency.get(NotificationGutsManager.class);
102     protected final NotificationRemoteInputManager mRemoteInputManager =
103             Dependency.get(NotificationRemoteInputManager.class);
104     protected final NotificationMediaManager mMediaManager =
105             Dependency.get(NotificationMediaManager.class);
106     protected final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
107     protected final DeviceProvisionedController mDeviceProvisionedController =
108             Dependency.get(DeviceProvisionedController.class);
109     protected final VisualStabilityManager mVisualStabilityManager =
110             Dependency.get(VisualStabilityManager.class);
111     protected final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
112     protected final ForegroundServiceController mForegroundServiceController =
113             Dependency.get(ForegroundServiceController.class);
114     protected final NotificationListener mNotificationListener =
115             Dependency.get(NotificationListener.class);
116     private final SmartReplyController mSmartReplyController =
117             Dependency.get(SmartReplyController.class);
118 
119     protected IStatusBarService mBarService;
120     protected NotificationPresenter mPresenter;
121     protected Callback mCallback;
122     protected PowerManager mPowerManager;
123     protected SystemServicesProxy mSystemServicesProxy;
124     protected NotificationListenerService.RankingMap mLatestRankingMap;
125     protected HeadsUpManager mHeadsUpManager;
126     protected NotificationData mNotificationData;
127     protected ContentObserver mHeadsUpObserver;
128     protected boolean mUseHeadsUp = false;
129     protected boolean mDisableNotificationAlerts;
130     protected NotificationListContainer mListContainer;
131     private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
132     /**
133      * Notifications with keys in this set are not actually around anymore. We kept them around
134      * when they were canceled in response to a remote input interaction. This allows us to show
135      * what you replied and allows you to continue typing into it.
136      */
137     private final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
138 
139 
140     private final class NotificationClicker implements View.OnClickListener {
141 
142         @Override
onClick(final View v)143         public void onClick(final View v) {
144             if (!(v instanceof ExpandableNotificationRow)) {
145                 Log.e(TAG, "NotificationClicker called on a view that is not a notification row.");
146                 return;
147             }
148 
149             mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), v);
150 
151             final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
152             final StatusBarNotification sbn = row.getStatusBarNotification();
153             if (sbn == null) {
154                 Log.e(TAG, "NotificationClicker called on an unclickable notification,");
155                 return;
156             }
157 
158             // Check if the notification is displaying the menu, if so slide notification back
159             if (row.getProvider() != null && row.getProvider().isMenuVisible()) {
160                 row.animateTranslateNotification(0);
161                 return;
162             }
163 
164             // Mark notification for one frame.
165             row.setJustClicked(true);
166             DejankUtils.postAfterTraversal(() -> row.setJustClicked(false));
167 
168             mCallback.onNotificationClicked(sbn, row);
169         }
170 
register(ExpandableNotificationRow row, StatusBarNotification sbn)171         public void register(ExpandableNotificationRow row, StatusBarNotification sbn) {
172             Notification notification = sbn.getNotification();
173             if (notification.contentIntent != null || notification.fullScreenIntent != null) {
174                 row.setOnClickListener(this);
175             } else {
176                 row.setOnClickListener(null);
177             }
178         }
179     }
180 
181     private final DeviceProvisionedController.DeviceProvisionedListener
182             mDeviceProvisionedListener =
183             new DeviceProvisionedController.DeviceProvisionedListener() {
184                 @Override
185                 public void onDeviceProvisionedChanged() {
186                     updateNotifications();
187                 }
188             };
189 
getLatestRankingMap()190     public NotificationListenerService.RankingMap getLatestRankingMap() {
191         return mLatestRankingMap;
192     }
193 
setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap)194     public void setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap) {
195         mLatestRankingMap = latestRankingMap;
196     }
197 
setDisableNotificationAlerts(boolean disableNotificationAlerts)198     public void setDisableNotificationAlerts(boolean disableNotificationAlerts) {
199         mDisableNotificationAlerts = disableNotificationAlerts;
200         mHeadsUpObserver.onChange(true);
201     }
202 
destroy()203     public void destroy() {
204         mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
205     }
206 
onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)207     public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
208         if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
209             removeNotification(entry.key, getLatestRankingMap());
210             mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
211             if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
212                 setLatestRankingMap(null);
213             }
214         } else {
215             updateNotificationRanking(null);
216         }
217     }
218 
219     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)220     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
221         pw.println("NotificationEntryManager state:");
222         pw.print("  mPendingNotifications=");
223         if (mPendingNotifications.size() == 0) {
224             pw.println("null");
225         } else {
226             for (NotificationData.Entry entry : mPendingNotifications.values()) {
227                 pw.println(entry.notification);
228             }
229         }
230         pw.print("  mUseHeadsUp=");
231         pw.println(mUseHeadsUp);
232         pw.print("  mKeysKeptForRemoteInput: ");
233         pw.println(mKeysKeptForRemoteInput);
234     }
235 
NotificationEntryManager(Context context)236     public NotificationEntryManager(Context context) {
237         mContext = context;
238         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
239         mBarService = IStatusBarService.Stub.asInterface(
240                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
241         mMessagingUtil = new NotificationMessagingUtil(context);
242         mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
243         mGroupManager.setPendingEntries(mPendingNotifications);
244     }
245 
setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer, Callback callback, HeadsUpManager headsUpManager)246     public void setUpWithPresenter(NotificationPresenter presenter,
247             NotificationListContainer listContainer, Callback callback,
248             HeadsUpManager headsUpManager) {
249         mPresenter = presenter;
250         mCallback = callback;
251         mNotificationData = new NotificationData(presenter);
252         mHeadsUpManager = headsUpManager;
253         mNotificationData.setHeadsUpManager(mHeadsUpManager);
254         mListContainer = listContainer;
255 
256         mHeadsUpObserver = new ContentObserver(mPresenter.getHandler()) {
257             @Override
258             public void onChange(boolean selfChange) {
259                 boolean wasUsing = mUseHeadsUp;
260                 mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
261                         && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
262                         mContext.getContentResolver(),
263                         Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
264                         Settings.Global.HEADS_UP_OFF);
265                 Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
266                 if (wasUsing != mUseHeadsUp) {
267                     if (!mUseHeadsUp) {
268                         Log.d(TAG,
269                                 "dismissing any existing heads up notification on disable event");
270                         mHeadsUpManager.releaseAllImmediately();
271                     }
272                 }
273             }
274         };
275 
276         if (ENABLE_HEADS_UP) {
277             mContext.getContentResolver().registerContentObserver(
278                     Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
279                     true,
280                     mHeadsUpObserver);
281             mContext.getContentResolver().registerContentObserver(
282                     Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
283                     mHeadsUpObserver);
284         }
285 
286         mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
287 
288         mHeadsUpObserver.onChange(true); // set up
289         mOnAppOpsClickListener = mGutsManager::openGuts;
290     }
291 
getNotificationData()292     public NotificationData getNotificationData() {
293         return mNotificationData;
294     }
295 
getNotificationLongClicker()296     public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() {
297         return mGutsManager::openGuts;
298     }
299 
300     @Override
logNotificationExpansion(String key, boolean userAction, boolean expanded)301     public void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
302         mUiOffloadThread.submit(() -> {
303             try {
304                 mBarService.onNotificationExpansionChanged(key, userAction, expanded);
305             } catch (RemoteException e) {
306                 // Ignore.
307             }
308         });
309     }
310 
311     @Override
onReorderingAllowed()312     public void onReorderingAllowed() {
313         updateNotifications();
314     }
315 
shouldSuppressFullScreenIntent(NotificationData.Entry entry)316     private boolean shouldSuppressFullScreenIntent(NotificationData.Entry entry) {
317         if (mPresenter.isDeviceInVrMode()) {
318             return true;
319         }
320 
321         return mNotificationData.shouldSuppressFullScreenIntent(entry);
322     }
323 
inflateViews(NotificationData.Entry entry, ViewGroup parent)324     private void inflateViews(NotificationData.Entry entry, ViewGroup parent) {
325         PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
326                 entry.notification.getUser().getIdentifier());
327 
328         final StatusBarNotification sbn = entry.notification;
329         if (entry.row != null) {
330             entry.reset();
331             updateNotification(entry, pmUser, sbn, entry.row);
332         } else {
333             new RowInflaterTask().inflate(mContext, parent, entry,
334                     row -> {
335                         bindRow(entry, pmUser, sbn, row);
336                         updateNotification(entry, pmUser, sbn, row);
337                     });
338         }
339     }
340 
bindRow(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row)341     private void bindRow(NotificationData.Entry entry, PackageManager pmUser,
342             StatusBarNotification sbn, ExpandableNotificationRow row) {
343         row.setExpansionLogger(this, entry.notification.getKey());
344         row.setGroupManager(mGroupManager);
345         row.setHeadsUpManager(mHeadsUpManager);
346         row.setOnExpandClickListener(mPresenter);
347         row.setInflationCallback(this);
348         row.setLongPressListener(getNotificationLongClicker());
349         mListContainer.bindRow(row);
350         mRemoteInputManager.bindRow(row);
351 
352         // Get the app name.
353         // Note that Notification.Builder#bindHeaderAppName has similar logic
354         // but since this field is used in the guts, it must be accurate.
355         // Therefore we will only show the application label, or, failing that, the
356         // package name. No substitutions.
357         final String pkg = sbn.getPackageName();
358         String appname = pkg;
359         try {
360             final ApplicationInfo info = pmUser.getApplicationInfo(pkg,
361                     PackageManager.MATCH_UNINSTALLED_PACKAGES
362                             | PackageManager.MATCH_DISABLED_COMPONENTS);
363             if (info != null) {
364                 appname = String.valueOf(pmUser.getApplicationLabel(info));
365             }
366         } catch (PackageManager.NameNotFoundException e) {
367             // Do nothing
368         }
369         row.setAppName(appname);
370         row.setOnDismissRunnable(() ->
371                 performRemoveNotification(row.getStatusBarNotification()));
372         row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
373         if (ENABLE_REMOTE_INPUT) {
374             row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
375         }
376 
377         row.setAppOpsOnClickListener(mOnAppOpsClickListener);
378 
379         mCallback.onBindRow(entry, pmUser, sbn, row);
380     }
381 
performRemoveNotification(StatusBarNotification n)382     public void performRemoveNotification(StatusBarNotification n) {
383         final int rank = mNotificationData.getRank(n.getKey());
384         final int count = mNotificationData.getActiveNotifications().size();
385         final NotificationVisibility nv = NotificationVisibility.obtain(n.getKey(), rank, count,
386                 true);
387         NotificationData.Entry entry = mNotificationData.get(n.getKey());
388 
389         if (FORCE_REMOTE_INPUT_HISTORY
390                 && mKeysKeptForRemoteInput.contains(n.getKey())) {
391             mKeysKeptForRemoteInput.remove(n.getKey());
392         }
393 
394         mRemoteInputManager.onPerformRemoveNotification(n, entry);
395         final String pkg = n.getPackageName();
396         final String tag = n.getTag();
397         final int id = n.getId();
398         final int userId = n.getUserId();
399         try {
400             int dismissalSurface = NotificationStats.DISMISSAL_SHADE;
401             if (isHeadsUp(n.getKey())) {
402                 dismissalSurface = NotificationStats.DISMISSAL_PEEK;
403             } else if (mListContainer.hasPulsingNotifications()) {
404                 dismissalSurface = NotificationStats.DISMISSAL_AOD;
405             }
406             mBarService.onNotificationClear(pkg, tag, id, userId, n.getKey(), dismissalSurface, nv);
407             removeNotification(n.getKey(), null);
408 
409         } catch (RemoteException ex) {
410             // system process is dead if we're here.
411         }
412 
413         mCallback.onPerformRemoveNotification(n);
414     }
415 
416     /**
417      * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
418      * about the failure.
419      *
420      * WARNING: this will call back into us.  Don't hold any locks.
421      */
handleNotificationError(StatusBarNotification n, String message)422     void handleNotificationError(StatusBarNotification n, String message) {
423         removeNotification(n.getKey(), null);
424         try {
425             mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
426                     n.getInitialPid(), message, n.getUserId());
427         } catch (RemoteException ex) {
428             // The end is nigh.
429         }
430     }
431 
abortExistingInflation(String key)432     private void abortExistingInflation(String key) {
433         if (mPendingNotifications.containsKey(key)) {
434             NotificationData.Entry entry = mPendingNotifications.get(key);
435             entry.abortTask();
436             mPendingNotifications.remove(key);
437         }
438         NotificationData.Entry addedEntry = mNotificationData.get(key);
439         if (addedEntry != null) {
440             addedEntry.abortTask();
441         }
442     }
443 
444     @Override
handleInflationException(StatusBarNotification notification, Exception e)445     public void handleInflationException(StatusBarNotification notification, Exception e) {
446         handleNotificationError(notification, e.getMessage());
447     }
448 
addEntry(NotificationData.Entry shadeEntry)449     private void addEntry(NotificationData.Entry shadeEntry) {
450         boolean isHeadsUped = shouldPeek(shadeEntry);
451         if (isHeadsUped) {
452             mHeadsUpManager.showNotification(shadeEntry);
453             // Mark as seen immediately
454             setNotificationShown(shadeEntry.notification);
455         }
456         addNotificationViews(shadeEntry);
457         mCallback.onNotificationAdded(shadeEntry);
458     }
459 
460     @Override
onAsyncInflationFinished(NotificationData.Entry entry)461     public void onAsyncInflationFinished(NotificationData.Entry entry) {
462         mPendingNotifications.remove(entry.key);
463         // If there was an async task started after the removal, we don't want to add it back to
464         // the list, otherwise we might get leaks.
465         boolean isNew = mNotificationData.get(entry.key) == null;
466         if (isNew && !entry.row.isRemoved()) {
467             addEntry(entry);
468         } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) {
469             mVisualStabilityManager.onLowPriorityUpdated(entry);
470             mPresenter.updateNotificationViews();
471         }
472         entry.row.setLowPriorityStateUpdated(false);
473     }
474 
475     @Override
removeNotification(String key, NotificationListenerService.RankingMap ranking)476     public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
477         boolean deferRemoval = false;
478         abortExistingInflation(key);
479         if (mHeadsUpManager.isHeadsUp(key)) {
480             // A cancel() in response to a remote input shouldn't be delayed, as it makes the
481             // sending look longer than it takes.
482             // Also we should not defer the removal if reordering isn't allowed since otherwise
483             // some notifications can't disappear before the panel is closed.
484             boolean ignoreEarliestRemovalTime = mRemoteInputManager.getController().isSpinning(key)
485                     && !FORCE_REMOTE_INPUT_HISTORY
486                     || !mVisualStabilityManager.isReorderingAllowed();
487             deferRemoval = !mHeadsUpManager.removeNotification(key,  ignoreEarliestRemovalTime);
488         }
489         mMediaManager.onNotificationRemoved(key);
490 
491         NotificationData.Entry entry = mNotificationData.get(key);
492         if (FORCE_REMOTE_INPUT_HISTORY
493                 && shouldKeepForRemoteInput(entry)
494                 && entry.row != null && !entry.row.isDismissed()) {
495             CharSequence remoteInputText = entry.remoteInputText;
496             if (TextUtils.isEmpty(remoteInputText)) {
497                 remoteInputText = entry.remoteInputTextWhenReset;
498             }
499             StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
500                     remoteInputText, false /* showSpinner */);
501             boolean updated = false;
502             entry.onRemoteInputInserted();
503             try {
504                 updateNotificationInternal(newSbn, null);
505                 updated = true;
506             } catch (InflationException e) {
507                 deferRemoval = false;
508             }
509             if (updated) {
510                 Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
511                 addKeyKeptForRemoteInput(entry.key);
512                 return;
513             }
514         }
515 
516         if (FORCE_REMOTE_INPUT_HISTORY
517                 && shouldKeepForSmartReply(entry)
518                 && entry.row != null && !entry.row.isDismissed()) {
519             // Turn off the spinner and hide buttons when an app cancels the notification.
520             StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
521             boolean updated = false;
522             try {
523                 updateNotificationInternal(newSbn, null);
524                 updated = true;
525             } catch (InflationException e) {
526                 // Ignore just don't keep the notification around.
527             }
528             // Treat the reply as longer sending.
529             mSmartReplyController.stopSending(entry);
530             if (updated) {
531                 Log.w(TAG, "Keeping notification around after sending smart reply " + entry.key);
532                 addKeyKeptForRemoteInput(entry.key);
533                 return;
534             }
535         }
536 
537         // Actually removing notification so smart reply controller can forget about it.
538         mSmartReplyController.stopSending(entry);
539 
540         if (deferRemoval) {
541             mLatestRankingMap = ranking;
542             mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
543             return;
544         }
545 
546         if (mRemoteInputManager.onRemoveNotification(entry)) {
547             mLatestRankingMap = ranking;
548             return;
549         }
550 
551         if (entry != null && mGutsManager.getExposedGuts() != null
552                 && mGutsManager.getExposedGuts() == entry.row.getGuts()
553                 && entry.row.getGuts() != null && !entry.row.getGuts().isLeavebehind()) {
554             Log.w(TAG, "Keeping notification because it's showing guts. " + key);
555             mLatestRankingMap = ranking;
556             mGutsManager.setKeyToRemoveOnGutsClosed(key);
557             return;
558         }
559 
560         if (entry != null) {
561             mForegroundServiceController.removeNotification(entry.notification);
562         }
563 
564         if (entry != null && entry.row != null) {
565             entry.row.setRemoved();
566             mListContainer.cleanUpViewState(entry.row);
567         }
568         // Let's remove the children if this was a summary
569         handleGroupSummaryRemoved(key);
570         StatusBarNotification old = removeNotificationViews(key, ranking);
571 
572         mCallback.onNotificationRemoved(key, old);
573     }
574 
rebuildNotificationWithRemoteInput(NotificationData.Entry entry, CharSequence remoteInputText, boolean showSpinner)575     public StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
576             CharSequence remoteInputText, boolean showSpinner) {
577         StatusBarNotification sbn = entry.notification;
578 
579         Notification.Builder b = Notification.Builder
580                 .recoverBuilder(mContext, sbn.getNotification().clone());
581         if (remoteInputText != null) {
582             CharSequence[] oldHistory = sbn.getNotification().extras
583                     .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
584             CharSequence[] newHistory;
585             if (oldHistory == null) {
586                 newHistory = new CharSequence[1];
587             } else {
588                 newHistory = new CharSequence[oldHistory.length + 1];
589                 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
590             }
591             newHistory[0] = String.valueOf(remoteInputText);
592             b.setRemoteInputHistory(newHistory);
593         }
594         b.setShowRemoteInputSpinner(showSpinner);
595         b.setHideSmartReplies(true);
596 
597         Notification newNotification = b.build();
598 
599         // Undo any compatibility view inflation
600         newNotification.contentView = sbn.getNotification().contentView;
601         newNotification.bigContentView = sbn.getNotification().bigContentView;
602         newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
603 
604         StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(),
605                 sbn.getOpPkg(),
606                 sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
607                 newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
608         return newSbn;
609     }
610 
611     @VisibleForTesting
rebuildNotificationForCanceledSmartReplies( NotificationData.Entry entry)612     StatusBarNotification rebuildNotificationForCanceledSmartReplies(
613             NotificationData.Entry entry) {
614         return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
615                 false /* showSpinner */);
616     }
617 
shouldKeepForSmartReply(NotificationData.Entry entry)618     private boolean shouldKeepForSmartReply(NotificationData.Entry entry) {
619         return entry != null && mSmartReplyController.isSendingSmartReply(entry.key);
620     }
621 
shouldKeepForRemoteInput(NotificationData.Entry entry)622     private boolean shouldKeepForRemoteInput(NotificationData.Entry entry) {
623         if (entry == null) {
624             return false;
625         }
626         if (mRemoteInputManager.getController().isSpinning(entry.key)) {
627             return true;
628         }
629         if (entry.hasJustSentRemoteInput()) {
630             return true;
631         }
632         return false;
633     }
634 
removeNotificationViews(String key, NotificationListenerService.RankingMap ranking)635     private StatusBarNotification removeNotificationViews(String key,
636             NotificationListenerService.RankingMap ranking) {
637         NotificationData.Entry entry = mNotificationData.remove(key, ranking);
638         if (entry == null) {
639             Log.w(TAG, "removeNotification for unknown key: " + key);
640             return null;
641         }
642         updateNotifications();
643         Dependency.get(LeakDetector.class).trackGarbage(entry);
644         return entry.notification;
645     }
646 
647     /**
648      * Ensures that the group children are cancelled immediately when the group summary is cancelled
649      * instead of waiting for the notification manager to send all cancels. Otherwise this could
650      * lead to flickers.
651      *
652      * This also ensures that the animation looks nice and only consists of a single disappear
653      * animation instead of multiple.
654      *  @param key the key of the notification was removed
655      *
656      */
handleGroupSummaryRemoved(String key)657     private void handleGroupSummaryRemoved(String key) {
658         NotificationData.Entry entry = mNotificationData.get(key);
659         if (entry != null && entry.row != null
660                 && entry.row.isSummaryWithChildren()) {
661             if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) {
662                 // We don't want to remove children for autobundled notifications as they are not
663                 // always cancelled. We only remove them if they were dismissed by the user.
664                 return;
665             }
666             List<ExpandableNotificationRow> notificationChildren =
667                     entry.row.getNotificationChildren();
668             for (int i = 0; i < notificationChildren.size(); i++) {
669                 ExpandableNotificationRow row = notificationChildren.get(i);
670                 if ((row.getStatusBarNotification().getNotification().flags
671                         & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
672                     // the child is a foreground service notification which we can't remove!
673                     continue;
674                 }
675                 row.setKeepInParent(true);
676                 // we need to set this state earlier as otherwise we might generate some weird
677                 // animations
678                 row.setRemoved();
679             }
680         }
681     }
682 
updateNotificationsOnDensityOrFontScaleChanged()683     public void updateNotificationsOnDensityOrFontScaleChanged() {
684         ArrayList<NotificationData.Entry> userNotifications =
685                 mNotificationData.getNotificationsForCurrentUser();
686         for (int i = 0; i < userNotifications.size(); i++) {
687             NotificationData.Entry entry = userNotifications.get(i);
688             boolean exposedGuts = mGutsManager.getExposedGuts() != null
689                     && entry.row.getGuts() == mGutsManager.getExposedGuts();
690             entry.row.onDensityOrFontScaleChanged();
691             if (exposedGuts) {
692                 mGutsManager.onDensityOrFontScaleChanged(entry.row);
693             }
694         }
695     }
696 
updateNotification(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row)697     protected void updateNotification(NotificationData.Entry entry, PackageManager pmUser,
698             StatusBarNotification sbn, ExpandableNotificationRow row) {
699         row.setNeedsRedaction(mLockscreenUserManager.needsRedaction(entry));
700         boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());
701         boolean isUpdate = mNotificationData.get(entry.key) != null;
702         boolean wasLowPriority = row.isLowPriority();
703         row.setIsLowPriority(isLowPriority);
704         row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority));
705         // bind the click event to the content area
706         mNotificationClicker.register(row, sbn);
707 
708         // Extract target SDK version.
709         try {
710             ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);
711             entry.targetSdk = info.targetSdkVersion;
712         } catch (PackageManager.NameNotFoundException ex) {
713             Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);
714         }
715         row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
716                 && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
717         entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
718         entry.autoRedacted = entry.notification.getNotification().publicVersion == null;
719 
720         entry.row = row;
721         entry.row.setOnActivatedListener(mPresenter);
722 
723         boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,
724                 mNotificationData.getImportance(sbn.getKey()));
725         boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight
726                 && !mPresenter.isPresenterFullyCollapsed();
727         row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
728         row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
729         row.updateNotification(entry);
730     }
731 
732 
addNotificationViews(NotificationData.Entry entry)733     protected void addNotificationViews(NotificationData.Entry entry) {
734         if (entry == null) {
735             return;
736         }
737         // Add the expanded view and icon.
738         mNotificationData.add(entry);
739         tagForeground(entry.notification);
740         updateNotifications();
741     }
742 
createNotificationViews(StatusBarNotification sbn)743     protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
744             throws InflationException {
745         if (DEBUG) {
746             Log.d(TAG, "createNotificationViews(notification=" + sbn);
747         }
748         NotificationData.Entry entry = new NotificationData.Entry(sbn);
749         Dependency.get(LeakDetector.class).trackInstance(entry);
750         entry.createIcons(mContext, sbn);
751         // Construct the expanded view.
752         inflateViews(entry, mListContainer.getViewParentForNotification(entry));
753         return entry;
754     }
755 
addNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)756     private void addNotificationInternal(StatusBarNotification notification,
757             NotificationListenerService.RankingMap ranking) throws InflationException {
758         String key = notification.getKey();
759         if (DEBUG) Log.d(TAG, "addNotification key=" + key);
760 
761         mNotificationData.updateRanking(ranking);
762         NotificationData.Entry shadeEntry = createNotificationViews(notification);
763         boolean isHeadsUped = shouldPeek(shadeEntry);
764         if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
765             if (shouldSuppressFullScreenIntent(shadeEntry)) {
766                 if (DEBUG) {
767                     Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key);
768                 }
769             } else if (mNotificationData.getImportance(key)
770                     < NotificationManager.IMPORTANCE_HIGH) {
771                 if (DEBUG) {
772                     Log.d(TAG, "No Fullscreen intent: not important enough: "
773                             + key);
774                 }
775             } else {
776                 // Stop screensaver if the notification has a fullscreen intent.
777                 // (like an incoming phone call)
778                 SystemServicesProxy.getInstance(mContext).awakenDreamsAsync();
779 
780                 // not immersive & a fullscreen alert should be shown
781                 if (DEBUG)
782                     Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
783                 try {
784                     EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
785                             key);
786                     notification.getNotification().fullScreenIntent.send();
787                     shadeEntry.notifyFullScreenIntentLaunched();
788                     mMetricsLogger.count("note_fullscreen", 1);
789                 } catch (PendingIntent.CanceledException e) {
790                 }
791             }
792         }
793         abortExistingInflation(key);
794 
795         mForegroundServiceController.addNotification(notification,
796                 mNotificationData.getImportance(key));
797 
798         mPendingNotifications.put(key, shadeEntry);
799         mGroupManager.onPendingEntryAdded(shadeEntry);
800     }
801 
802     @VisibleForTesting
tagForeground(StatusBarNotification notification)803     protected void tagForeground(StatusBarNotification notification) {
804         ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps(
805                 notification.getUserId(), notification.getPackageName());
806         if (activeOps != null) {
807             int N = activeOps.size();
808             for (int i = 0; i < N; i++) {
809                 updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(),
810                         notification.getPackageName(), true);
811             }
812         }
813     }
814 
815     @Override
addNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)816     public void addNotification(StatusBarNotification notification,
817             NotificationListenerService.RankingMap ranking) {
818         try {
819             addNotificationInternal(notification, ranking);
820         } catch (InflationException e) {
821             handleInflationException(notification, e);
822         }
823     }
824 
updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon)825     public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) {
826         String foregroundKey = mForegroundServiceController.getStandardLayoutKey(
827                 UserHandle.getUserId(uid), pkg);
828         if (foregroundKey != null) {
829             mNotificationData.updateAppOp(appOp, uid, pkg, foregroundKey, showIcon);
830             updateNotifications();
831         }
832     }
833 
alertAgain(NotificationData.Entry oldEntry, Notification newNotification)834     private boolean alertAgain(NotificationData.Entry oldEntry, Notification newNotification) {
835         return oldEntry == null || !oldEntry.hasInterrupted()
836                 || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
837     }
838 
updateNotificationInternal(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)839     private void updateNotificationInternal(StatusBarNotification notification,
840             NotificationListenerService.RankingMap ranking) throws InflationException {
841         if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
842 
843         final String key = notification.getKey();
844         abortExistingInflation(key);
845         NotificationData.Entry entry = mNotificationData.get(key);
846         if (entry == null) {
847             return;
848         }
849         mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
850         mRemoteInputManager.onUpdateNotification(entry);
851         mSmartReplyController.stopSending(entry);
852 
853         if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) {
854             mGutsManager.setKeyToRemoveOnGutsClosed(null);
855             Log.w(TAG, "Notification that was kept for guts was updated. " + key);
856         }
857 
858         Notification n = notification.getNotification();
859         mNotificationData.updateRanking(ranking);
860 
861         final StatusBarNotification oldNotification = entry.notification;
862         entry.notification = notification;
863         mGroupManager.onEntryUpdated(entry, oldNotification);
864 
865         entry.updateIcons(mContext, notification);
866         inflateViews(entry, mListContainer.getViewParentForNotification(entry));
867 
868         mForegroundServiceController.updateNotification(notification,
869                 mNotificationData.getImportance(key));
870 
871         boolean shouldPeek = shouldPeek(entry, notification);
872         boolean alertAgain = alertAgain(entry, n);
873 
874         updateHeadsUp(key, entry, shouldPeek, alertAgain);
875         updateNotifications();
876 
877         if (!notification.isClearable()) {
878             // The user may have performed a dismiss action on the notification, since it's
879             // not clearable we should snap it back.
880             mListContainer.snapViewIfNeeded(entry.row);
881         }
882 
883         if (DEBUG) {
884             // Is this for you?
885             boolean isForCurrentUser = mPresenter.isNotificationForCurrentProfiles(notification);
886             Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
887         }
888 
889         mCallback.onNotificationUpdated(notification);
890     }
891 
892     @Override
updateNotification(StatusBarNotification notification, NotificationListenerService.RankingMap ranking)893     public void updateNotification(StatusBarNotification notification,
894             NotificationListenerService.RankingMap ranking) {
895         try {
896             updateNotificationInternal(notification, ranking);
897         } catch (InflationException e) {
898             handleInflationException(notification, e);
899         }
900     }
901 
updateNotifications()902     public void updateNotifications() {
903         mNotificationData.filterAndSort();
904 
905         mPresenter.updateNotificationViews();
906     }
907 
updateNotificationRanking(NotificationListenerService.RankingMap ranking)908     public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) {
909         mNotificationData.updateRanking(ranking);
910         updateNotifications();
911     }
912 
shouldPeek(NotificationData.Entry entry)913     protected boolean shouldPeek(NotificationData.Entry entry) {
914         return shouldPeek(entry, entry.notification);
915     }
916 
shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn)917     public boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) {
918         if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) {
919             if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode");
920             return false;
921         }
922 
923         if (mNotificationData.shouldFilterOut(entry)) {
924             if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey());
925             return false;
926         }
927 
928         boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming();
929 
930         if (!inUse && !mPresenter.isDozing()) {
931             if (DEBUG) {
932                 Log.d(TAG, "No peeking: not in use: " + sbn.getKey());
933             }
934             return false;
935         }
936 
937         if (!mPresenter.isDozing() && mNotificationData.shouldSuppressPeek(entry)) {
938             if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
939             return false;
940         }
941 
942         // Peeking triggers an ambient display pulse, so disable peek is ambient is active
943         if (mPresenter.isDozing() && mNotificationData.shouldSuppressAmbient(entry)) {
944             if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
945             return false;
946         }
947 
948         if (entry.hasJustLaunchedFullScreenIntent()) {
949             if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey());
950             return false;
951         }
952 
953         if (isSnoozedPackage(sbn)) {
954             if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey());
955             return false;
956         }
957 
958         // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
959         int importanceLevel = mPresenter.isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
960                 : NotificationManager.IMPORTANCE_HIGH;
961         if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
962             if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
963             return false;
964         }
965 
966         // Don't peek notifications that are suppressed due to group alert behavior
967         if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
968             if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior");
969             return false;
970         }
971 
972         if (!mCallback.shouldPeek(entry, sbn)) {
973             return false;
974         }
975 
976         return true;
977     }
978 
setNotificationShown(StatusBarNotification n)979     protected void setNotificationShown(StatusBarNotification n) {
980         setNotificationsShown(new String[]{n.getKey()});
981     }
982 
setNotificationsShown(String[] keys)983     protected void setNotificationsShown(String[] keys) {
984         try {
985             mNotificationListener.setNotificationsShown(keys);
986         } catch (RuntimeException e) {
987             Log.d(TAG, "failed setNotificationsShown: ", e);
988         }
989     }
990 
isSnoozedPackage(StatusBarNotification sbn)991     protected boolean isSnoozedPackage(StatusBarNotification sbn) {
992         return mHeadsUpManager.isSnoozed(sbn.getPackageName());
993     }
994 
updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldPeek, boolean alertAgain)995     protected void updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldPeek,
996             boolean alertAgain) {
997         final boolean wasHeadsUp = isHeadsUp(key);
998         if (wasHeadsUp) {
999             if (!shouldPeek) {
1000                 // We don't want this to be interrupting anymore, lets remove it
1001                 mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
1002             } else {
1003                 mHeadsUpManager.updateNotification(entry, alertAgain);
1004             }
1005         } else if (shouldPeek && alertAgain) {
1006             // This notification was updated to be a heads-up, show it!
1007             mHeadsUpManager.showNotification(entry);
1008         }
1009     }
1010 
isHeadsUp(String key)1011     protected boolean isHeadsUp(String key) {
1012         return mHeadsUpManager.isHeadsUp(key);
1013     }
1014 
isNotificationKeptForRemoteInput(String key)1015     public boolean isNotificationKeptForRemoteInput(String key) {
1016         return mKeysKeptForRemoteInput.contains(key);
1017     }
1018 
removeKeyKeptForRemoteInput(String key)1019     public void removeKeyKeptForRemoteInput(String key) {
1020         mKeysKeptForRemoteInput.remove(key);
1021     }
1022 
addKeyKeptForRemoteInput(String key)1023     public void addKeyKeptForRemoteInput(String key) {
1024         if (FORCE_REMOTE_INPUT_HISTORY) {
1025             mKeysKeptForRemoteInput.add(key);
1026         }
1027     }
1028 
1029     /**
1030      * Callback for NotificationEntryManager.
1031      */
1032     public interface Callback {
1033 
1034         /**
1035          * Called when a new entry is created.
1036          *
1037          * @param shadeEntry entry that was created
1038          */
onNotificationAdded(NotificationData.Entry shadeEntry)1039         void onNotificationAdded(NotificationData.Entry shadeEntry);
1040 
1041         /**
1042          * Called when a notification was updated.
1043          *
1044          * @param notification notification that was updated
1045          */
onNotificationUpdated(StatusBarNotification notification)1046         void onNotificationUpdated(StatusBarNotification notification);
1047 
1048         /**
1049          * Called when a notification was removed.
1050          *
1051          * @param key key of notification that was removed
1052          * @param old StatusBarNotification of the notification before it was removed
1053          */
onNotificationRemoved(String key, StatusBarNotification old)1054         void onNotificationRemoved(String key, StatusBarNotification old);
1055 
1056 
1057         /**
1058          * Called when a notification is clicked.
1059          *
1060          * @param sbn notification that was clicked
1061          * @param row row for that notification
1062          */
onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row)1063         void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row);
1064 
1065         /**
1066          * Called when a new notification and row is created.
1067          *
1068          * @param entry entry for the notification
1069          * @param pmUser package manager for user
1070          * @param sbn notification
1071          * @param row row for the notification
1072          */
onBindRow(NotificationData.Entry entry, PackageManager pmUser, StatusBarNotification sbn, ExpandableNotificationRow row)1073         void onBindRow(NotificationData.Entry entry, PackageManager pmUser,
1074                 StatusBarNotification sbn, ExpandableNotificationRow row);
1075 
1076         /**
1077          * Removes a notification immediately.
1078          *
1079          * @param statusBarNotification notification that is being removed
1080          */
onPerformRemoveNotification(StatusBarNotification statusBarNotification)1081         void onPerformRemoveNotification(StatusBarNotification statusBarNotification);
1082 
1083         /**
1084          * Returns true if NotificationEntryManager should peek this notification.
1085          *
1086          * @param entry entry of the notification that might be peeked
1087          * @param sbn notification that might be peeked
1088          * @return true if the notification should be peeked
1089          */
shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn)1090         boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn);
1091     }
1092 }
1093