From 331a64d091d68ad59baad55e7c5f86d391a4fc54 Mon Sep 17 00:00:00 2001 From: Geoffrey Pitsch Date: Tue, 17 Jan 2017 14:00:47 -0500 Subject: [PATCH] Fix cancel notification race condition bug. Cancel should always remove notifications, regardless of whether they've been posted internally. Primarily accomplished by ensuring all cancels happen on the same handler, and that they check the enqueued list as well the main notifications list. Also fixes snoozing for enqueued notifications, since it is implemented via cancelling. Test: runtest systemui-notification Change-Id: If8295a38102f245013d29831a060fabd80716e4b --- .../NotificationManagerService.java | 591 ++++++++++-------- .../tests/notification/AndroidManifest.xml | 1 + .../NotificationManagerServiceTest.java | 190 +++++- 3 files changed, 500 insertions(+), 282 deletions(-) diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6c66a60d530fd..323b468d9b48f 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -198,6 +198,8 @@ public class NotificationManagerService extends SystemService { static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; + static final long SNOOZE_UNTIL_UNSPECIFIED = -1; + static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION; @@ -229,6 +231,7 @@ public class NotificationManagerService extends SystemService { private IActivityManager mAm; private IPackageManager mPackageManager; + private PackageManager mPackageManagerClient; AudioManager mAudioManager; AudioManagerInternal mAudioManagerInternal; @Nullable StatusBarManagerInternal mStatusBar; @@ -268,6 +271,7 @@ public class NotificationManagerService extends SystemService { private boolean mNotificationPulseEnabled; // used as a mutex for access to all active notifications & listeners + final Object mNotificationLock = new Object(); final ArrayList mNotificationList = new ArrayList(); final ArrayMap mNotificationsByKey = @@ -372,7 +376,7 @@ public class NotificationManagerService extends SystemService { private void loadPolicyFile() { if (DBG) Slog.d(TAG, "loadPolicyFile"); - synchronized(mPolicyFile) { + synchronized (mPolicyFile) { FileInputStream infile = null; try { @@ -491,7 +495,7 @@ public class NotificationManagerService extends SystemService { @Override public void onSetDisabled(int status) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mDisableNotificationEffects = (status & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0; if (disableNotificationEffects(null) != null) { @@ -519,7 +523,7 @@ public class NotificationManagerService extends SystemService { @Override public void onClearAll(int callingUid, int callingPid, int userId) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { cancelAllLocked(callingUid, callingPid, userId, REASON_DELEGATE_CANCEL_ALL, null, /*includeCurrentProfiles*/ true); } @@ -527,7 +531,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationClick(int callingUid, int callingPid, String key) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r == null) { Log.w(TAG, "No notification with key: " + key); @@ -548,7 +552,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationActionClick(int callingUid, int callingPid, String key, int actionIndex) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r == null) { Log.w(TAG, "No notification with key: " + key); @@ -584,7 +588,7 @@ public class NotificationManagerService extends SystemService { @Override public void clearEffects() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { if (DBG) Slog.d(TAG, "clearEffects"); clearSoundLocked(); clearVibrateLocked(); @@ -612,7 +616,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationVisibilityChanged(NotificationVisibility[] newlyVisibleKeys, NotificationVisibility[] noLongerVisibleKeys) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { for (NotificationVisibility nv : newlyVisibleKeys) { NotificationRecord r = mNotificationsByKey.get(nv.key); if (r == null) continue; @@ -635,7 +639,7 @@ public class NotificationManagerService extends SystemService { @Override public void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord r = mNotificationsByKey.get(key); if (r != null) { r.stats.onExpansionChanged(userAction, expanded); @@ -949,7 +953,8 @@ public class NotificationManagerService extends SystemService { // TODO: Tests should call onStart instead once the methods above are removed. @VisibleForTesting - void init(IPackageManager packageManager, LightsManager lightsManager) { + void init(Looper looper, IPackageManager packageManager, PackageManager packageManagerClient, + LightsManager lightsManager, NotificationListeners notificationListeners) { Resources resources = getContext().getResources(); mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, @@ -957,11 +962,12 @@ public class NotificationManagerService extends SystemService { mAm = ActivityManager.getService(); mPackageManager = packageManager; + mPackageManagerClient = packageManagerClient; mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); - mHandler = new WorkerHandler(); + mHandler = new WorkerHandler(looper); mRankingThread.start(); String[] extractorNames; try { @@ -991,7 +997,7 @@ public class NotificationManagerService extends SystemService { new Intent(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT), UserHandle.ALL, android.Manifest.permission.MANAGE_NOTIFICATIONS); - synchronized(mNotificationList) { + synchronized (mNotificationLock) { updateInterruptionFilterLocked(); } } @@ -1019,7 +1025,7 @@ public class NotificationManagerService extends SystemService { mGroupHelper = new GroupHelper(new GroupHelper.Callback() { @Override public void addAutoGroup(String key) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { addAutogroupKeyLocked(key); } mRankingHandler.requestSort(false); @@ -1027,7 +1033,7 @@ public class NotificationManagerService extends SystemService { @Override public void removeAutoGroup(String key) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { removeAutogroupKeyLocked(key); } mRankingHandler.requestSort(false); @@ -1040,7 +1046,7 @@ public class NotificationManagerService extends SystemService { @Override public void removeAutoGroupSummary(int userId, String pkg) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { clearAutogroupSummaryLocked(userId, pkg); } } @@ -1051,8 +1057,8 @@ public class NotificationManagerService extends SystemService { syncBlockDb(); - // This is a MangedServices object that keeps track of the listeners. - mListeners = new NotificationListeners(); + // This is a ManagedServices object that keeps track of the listeners. + mListeners = notificationListeners; // This is a MangedServices object that keeps track of the assistant. mNotificationAssistants = new NotificationAssistants(); @@ -1134,7 +1140,8 @@ public class NotificationManagerService extends SystemService { @Override public void onStart() { - init(AppGlobals.getPackageManager(), getLocalService(LightsManager.class)); + init(Looper.myLooper(), AppGlobals.getPackageManager(), getContext().getPackageManager(), + getLocalService(LightsManager.class), new NotificationListeners()); publishBinderService(Context.NOTIFICATION_SERVICE, mService); publishLocalService(NotificationManagerInternal.class, mInternalService); } @@ -1627,7 +1634,7 @@ public class NotificationManagerService extends SystemService { // noteOp will check to make sure the callingPkg matches the uid if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg) == AppOpsManager.MODE_ALLOWED) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { tmp = new StatusBarNotification[mNotificationList.size()]; final int N = mNotificationList.size(); for (int i=0; i map = new ArrayMap<>(mNotificationList.size() + mEnqueuedNotifications.size()); - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final int N = mNotificationList.size(); for (int i = 0; i < N; i++) { StatusBarNotification sbn = sanitizeSbn(pkg, userId, @@ -1668,10 +1675,8 @@ public class NotificationManagerService extends SystemService { map.put(sbn.getKey(), sbn); } } - } - synchronized (mEnqueuedNotifications) { - final int N = mEnqueuedNotifications.size(); - for (int i = 0; i < N; i++) { + final int M = mEnqueuedNotifications.size(); + for (int i = 0; i < M; i++) { StatusBarNotification sbn = sanitizeSbn(pkg, userId, mEnqueuedNotifications.get(i).sbn); if (sbn != null) { @@ -1763,7 +1768,7 @@ public class NotificationManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (keys != null) { final int N = keys.length; @@ -1826,7 +1831,7 @@ public class NotificationManagerService extends SystemService { public void setNotificationsShownFromListener(INotificationListener token, String[] keys) { long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (keys != null) { final int N = keys.length; @@ -1881,7 +1886,7 @@ public class NotificationManagerService extends SystemService { long identity = Binder.clearCallingIdentity(); try { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, snoozeCriterionId, info); + snoozeNotificationInt(key, SNOOZE_UNTIL_UNSPECIFIED, snoozeCriterionId, info); } finally { Binder.restoreCallingIdentity(identity); } @@ -1898,7 +1903,7 @@ public class NotificationManagerService extends SystemService { long identity = Binder.clearCallingIdentity(); try { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, snoozeUntil, info); + snoozeNotificationInt(key, snoozeUntil, null, info); } finally { Binder.restoreCallingIdentity(identity); } @@ -1914,7 +1919,7 @@ public class NotificationManagerService extends SystemService { long identity = Binder.clearCallingIdentity(); try { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, info); + snoozeNotificationInt(key, SNOOZE_UNTIL_UNSPECIFIED, null, info); } finally { Binder.restoreCallingIdentity(identity); } @@ -1950,7 +1955,7 @@ public class NotificationManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (info.supportsProfiles()) { Log.e(TAG, "Ignoring deprecated cancelNotification(pkg, tag, id) " @@ -1979,7 +1984,7 @@ public class NotificationManagerService extends SystemService { @Override public ParceledListSlice getActiveNotificationsFromListener( INotificationListener token, String[] keys, int trim) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); final boolean getKeys = keys != null; final int N = getKeys ? keys.length : mNotificationList.size(); @@ -2004,7 +2009,7 @@ public class NotificationManagerService extends SystemService { public void requestHintsFromListener(INotificationListener token, int hints) { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); final int disableEffectsMask = HINT_HOST_DISABLE_EFFECTS | HINT_HOST_DISABLE_NOTIFICATION_EFFECTS @@ -2025,7 +2030,7 @@ public class NotificationManagerService extends SystemService { @Override public int getHintsFromListener(INotificationListener token) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { return mListenerHints; } } @@ -2035,7 +2040,7 @@ public class NotificationManagerService extends SystemService { int interruptionFilter) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); mZenModeHelper.requestFromListener(info.component, interruptionFilter); updateInterruptionFilterLocked(); @@ -2056,7 +2061,7 @@ public class NotificationManagerService extends SystemService { @Override public void setOnNotificationPostedTrimFromListener(INotificationListener token, int trim) throws RemoteException { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); if (info == null) return; mListeners.setOnNotificationPostedTrimLocked(info, trim); @@ -2375,7 +2380,7 @@ public class NotificationManagerService extends SystemService { enforceSystemOrSystemUI("grant notification policy access"); final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mPolicyAccess.put(pkg, granted); } } finally { @@ -2410,7 +2415,7 @@ public class NotificationManagerService extends SystemService { Adjustment adjustment) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { mNotificationAssistants.checkServiceTokenLocked(token); int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { @@ -2432,7 +2437,7 @@ public class NotificationManagerService extends SystemService { Adjustment adjustment) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mNotificationAssistants.checkServiceTokenLocked(token); NotificationRecord n = mNotificationsByKey.get(adjustment.getKey()); applyAdjustment(n, adjustment); @@ -2449,7 +2454,7 @@ public class NotificationManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mNotificationAssistants.checkServiceTokenLocked(token); for (Adjustment adjustment : adjustments) { NotificationRecord n = mNotificationsByKey.get(adjustment.getKey()); @@ -2555,9 +2560,8 @@ public class NotificationManagerService extends SystemService { ArrayMap summaries = mAutobundledSummaries.get(userId); if (summaries != null && summaries.containsKey(pkg)) { // Clear summary. - final NotificationRecord removed = mNotificationsByKey.get(summaries.remove(pkg)); + final NotificationRecord removed = findNotificationByKeyLocked(summaries.remove(pkg)); if (removed != null) { - mNotificationList.remove(removed); cancelNotificationLocked(removed, false, REASON_UNAUTOBUNDLED); } } @@ -2566,7 +2570,7 @@ public class NotificationManagerService extends SystemService { // Posts a 'fake' summary for a package that has exceeded the solo-notification limit. private void createAutoGroupSummary(int userId, String pkg, String triggeringKey) { NotificationRecord summaryRecord = null; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { NotificationRecord notificationRecord = mNotificationsByKey.get(triggeringKey); if (notificationRecord == null) { // The notification could have been cancelled again already. A successive @@ -2617,7 +2621,7 @@ public class NotificationManagerService extends SystemService { } } if (summaryRecord != null) { - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { mEnqueuedNotifications.add(summaryRecord); } mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord)); @@ -2672,7 +2676,7 @@ public class NotificationManagerService extends SystemService { } } - synchronized (mNotificationList) { + synchronized (mNotificationLock) { if (!zenOnly) { N = mNotificationList.size(); if (N > 0) { @@ -2710,19 +2714,17 @@ public class NotificationManagerService extends SystemService { } pw.println(" mArchive=" + mArchive.toString()); Iterator iter = mArchive.descendingIterator(); - int i=0; + int j=0; while (iter.hasNext()) { final StatusBarNotification sbn = iter.next(); if (filter != null && !filter.matches(sbn)) continue; pw.println(" " + sbn); - if (++i >= 5) { + if (++j >= 5) { if (iter.hasNext()) pw.println(" ..."); break; } } - } - synchronized (mEnqueuedNotifications) { if (!zenOnly) { N = mEnqueuedNotifications.size(); if (N > 0) { @@ -2817,14 +2819,14 @@ public class NotificationManagerService extends SystemService { public void removeForegroundServiceFlagFromNotification(String pkg, int notificationId, int userId) { checkCallerIsSystem(); - synchronized (mNotificationList) { - int i = indexOfNotificationLocked(pkg, null, notificationId, userId); - if (i < 0) { + synchronized (mNotificationLock) { + NotificationRecord r = findNotificationByListLocked(mNotificationList, pkg, null, + notificationId, userId); + if (r == null) { Log.d(TAG, "stripForegroundServiceFlag: Could not find notification with " + "pkg=" + pkg + " / id=" + notificationId + " / userId=" + userId); return; } - NotificationRecord r = mNotificationList.get(i); StatusBarNotification sbn = r.sbn; // NoMan adds flags FLAG_NO_CLEAR and FLAG_ONGOING_EVENT when it sees // FLAG_FOREGROUND_SERVICE. Hence it's not enough to remove FLAG_FOREGROUND_SERVICE, @@ -2861,7 +2863,7 @@ public class NotificationManagerService extends SystemService { // Fix the notification as best we can. try { - final ApplicationInfo ai = getContext().getPackageManager().getApplicationInfoAsUser( + final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser( pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, (userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId); Notification.addFieldsFromContext(ai, userId, notification); @@ -2886,8 +2888,8 @@ public class NotificationManagerService extends SystemService { // Limit the number of notifications that any given package except the android // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { - synchronized (mNotificationList) { - if(mNotificationsByKey.get(n.getKey()) != null) { + synchronized (mNotificationLock) { + if (mNotificationsByKey.get(n.getKey()) != null) { // this is an update, rate limit updates only final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg); if (appEnqueueRate > mMaxPackageEnqueueRate) { @@ -2945,7 +2947,7 @@ public class NotificationManagerService extends SystemService { // setup local book-keeping final NotificationRecord r = new NotificationRecord(getContext(), n); - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { mEnqueuedNotifications.add(r); } mHandler.post(new EnqueueNotificationRunnable(userId, r)); @@ -2964,7 +2966,7 @@ public class NotificationManagerService extends SystemService { @Override public void run() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { if (mSnoozeHelper.isSnoozed(userId, r.sbn.getPackageName(), r.getKey())) { // TODO: log to event log if (DBG) { @@ -3059,9 +3061,9 @@ public class NotificationManagerService extends SystemService { @Override public void run() { - try { - NotificationRecord r = null; - synchronized (mEnqueuedNotifications) { + synchronized (mNotificationLock) { + try { + NotificationRecord r = null; int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { final NotificationRecord enqueued = mEnqueuedNotifications.get(i); @@ -3070,12 +3072,10 @@ public class NotificationManagerService extends SystemService { break; } } - } - if (r == null) { - Slog.e(TAG, "Cannot find enqueued record for key: " + key); - return; - } - synchronized (mNotificationList) { + if (r == null) { + Slog.i(TAG, "Cannot find enqueued record for key: " + key); + return; + } NotificationRecord old = mNotificationsByKey.get(key); final StatusBarNotification n = r.sbn; final Notification notification = n.getNotification(); @@ -3134,9 +3134,7 @@ public class NotificationManagerService extends SystemService { } buzzBeepBlinkLocked(r); - } - } finally { - synchronized (mEnqueuedNotifications) { + } finally { int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { final NotificationRecord enqueued = mEnqueuedNotifications.get(i); @@ -3193,8 +3191,7 @@ public class NotificationManagerService extends SystemService { // notification was a summary and the new one isn't, or when the old // notification was a summary and its group key changed. if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) { - cancelGroupChildrenLocked(old, callingUid, callingPid, null, - REASON_GROUP_SUMMARY_CANCELED, false /* sendDelete */); + cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */); } } @@ -3463,7 +3460,7 @@ public class NotificationManagerService extends SystemService { RankingReconsideration recon = (RankingReconsideration) message.obj; recon.run(); boolean changed; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final NotificationRecord record = mNotificationsByKey.get(recon.getKey()); if (record == null) { return; @@ -3491,7 +3488,7 @@ public class NotificationManagerService extends SystemService { private void handleRankingSort(Message msg) { if (!(msg.obj instanceof Boolean)) return; boolean forceUpdate = ((Boolean) msg.obj == null) ? false : (boolean) msg.obj; - synchronized (mNotificationList) { + synchronized (mNotificationLock) { final int N = mNotificationList.size(); ArrayList orderBefore = new ArrayList(N); ArrayList groupOverrideBefore = new ArrayList<>(N); @@ -3548,7 +3545,7 @@ public class NotificationManagerService extends SystemService { } private void handleSendRankingUpdate() { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mListeners.notifyRankingUpdateLocked(); } } @@ -3567,19 +3564,23 @@ public class NotificationManagerService extends SystemService { } private void handleListenerHintsChanged(int hints) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mListeners.notifyListenerHintsChangedLocked(hints); } } private void handleListenerInterruptionFilterChanged(int interruptionFilter) { - synchronized (mNotificationList) { + synchronized (mNotificationLock) { mListeners.notifyInterruptionFilterChanged(interruptionFilter); } } private final class WorkerHandler extends Handler { + public WorkerHandler(Looper looper) { + super(looper); + } + @Override public void handleMessage(Message msg) { @@ -3665,6 +3666,17 @@ public class NotificationManagerService extends SystemService { } private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) { + final String canceledKey = r.getKey(); + + // Remove from either list + boolean wasPosted; + if (mNotificationList.remove(r)) { + mNotificationsByKey.remove(r.sbn.getKey()); + wasPosted = true; + } else { + mEnqueuedNotifications.remove(r); + wasPosted = false; + } // Record caller. recordCallerLocked(r); @@ -3682,50 +3694,51 @@ public class NotificationManagerService extends SystemService { } } - // status bar - if (r.getNotification().getSmallIcon() != null) { - r.isCanceled = true; - mListeners.notifyRemovedLocked(r.sbn, reason); - mHandler.post(new Runnable() { - @Override - public void run() { - mGroupHelper.onNotificationRemoved(r.sbn); + // Only cancel these if this notification actually got to be posted. + if (wasPosted) { + // status bar + if (r.getNotification().getSmallIcon() != null) { + r.isCanceled = true; + mListeners.notifyRemovedLocked(r.sbn, reason); + mHandler.post(new Runnable() { + @Override + public void run() { + mGroupHelper.onNotificationRemoved(r.sbn); + } + }); + } + + // sound + if (canceledKey.equals(mSoundNotificationKey)) { + mSoundNotificationKey = null; + final long identity = Binder.clearCallingIdentity(); + try { + final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); + if (player != null) { + player.stopAsync(); + } + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(identity); } - }); - } + } - final String canceledKey = r.getKey(); - - // sound - if (canceledKey.equals(mSoundNotificationKey)) { - mSoundNotificationKey = null; - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - player.stopAsync(); + // vibrate + if (canceledKey.equals(mVibrateNotificationKey)) { + mVibrateNotificationKey = null; + long identity = Binder.clearCallingIdentity(); + try { + mVibrator.cancel(); + } + finally { + Binder.restoreCallingIdentity(identity); } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); } - } - // vibrate - if (canceledKey.equals(mVibrateNotificationKey)) { - mVibrateNotificationKey = null; - long identity = Binder.clearCallingIdentity(); - try { - mVibrator.cancel(); - } - finally { - Binder.restoreCallingIdentity(identity); - } + // light + mLights.remove(canceledKey); } - // light - mLights.remove(canceledKey); - // Record usage stats // TODO: add unbundling stats? switch (reason) { @@ -3741,10 +3754,9 @@ public class NotificationManagerService extends SystemService { break; } - mNotificationsByKey.remove(r.sbn.getKey()); String groupKey = r.getGroupKey(); NotificationRecord groupSummary = mSummaryByGroupKey.get(groupKey); - if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) { + if (groupSummary != null && groupSummary.getKey().equals(canceledKey)) { mSummaryByGroupKey.remove(groupKey); } final ArrayMap summaries = mAutobundledSummaries.get(r.sbn.getUserId()); @@ -3779,10 +3791,11 @@ public class NotificationManagerService extends SystemService { if (DBG) EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, id, tag, userId, mustHaveFlags, mustNotHaveFlags, reason, listenerName); - synchronized (mNotificationList) { - int index = indexOfNotificationLocked(pkg, tag, id, userId); - if (index >= 0) { - NotificationRecord r = mNotificationList.get(index); + synchronized (mNotificationLock) { + // Look for the notification, searching both the posted and enqueued lists. + NotificationRecord r = findNotificationLocked(pkg, tag, id, userId); + if (r != null) { + // The notification was found, check if it should be removed. // Ideally we'd do this in the caller of this method. However, that would // require the caller to also find the notification. @@ -3797,13 +3810,13 @@ public class NotificationManagerService extends SystemService { return; } - mNotificationList.remove(index); - + // Cancel the notification. cancelNotificationLocked(r, sendDelete, reason); cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName, - REASON_GROUP_SUMMARY_CANCELED, sendDelete); + sendDelete); updateLightsLocked(); } else { + // No notification was found, assume that it is snoozed and cancel it. final boolean wasSnoozed = mSnoozeHelper.cancel(userId, pkg, tag, id); if (wasSnoozed) { savePolicyFile(); @@ -3842,120 +3855,131 @@ public class NotificationManagerService extends SystemService { * Cancels all notifications from a given package that have all of the * {@code mustHaveFlags}. */ - boolean cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, String channelId, + void cancelAllNotificationsInt(int callingUid, int callingPid, String pkg, String channelId, int mustHaveFlags, int mustNotHaveFlags, boolean doit, int userId, int reason, ManagedServiceInfo listener) { - String listenerName = listener == null ? null : listener.component.toShortString(); - EventLogTags.writeNotificationCancelAll(callingUid, callingPid, - pkg, userId, mustHaveFlags, mustNotHaveFlags, reason, - listenerName); + mHandler.post(new Runnable() { + @Override + public void run() { + String listenerName = listener == null ? null : listener.component.toShortString(); + EventLogTags.writeNotificationCancelAll(callingUid, callingPid, + pkg, userId, mustHaveFlags, mustNotHaveFlags, reason, + listenerName); - synchronized (mNotificationList) { - final int N = mNotificationList.size(); - ArrayList canceledNotifications = null; - for (int i = N-1; i >= 0; --i) { - NotificationRecord r = mNotificationList.get(i); - if (!notificationMatchesUserId(r, userId)) { - continue; - } - // Don't remove notifications to all, if there's no package name specified - if (r.getUserId() == UserHandle.USER_ALL && pkg == null) { - continue; - } - if ((r.getFlags() & mustHaveFlags) != mustHaveFlags) { - continue; - } - if ((r.getFlags() & mustNotHaveFlags) != 0) { - continue; - } - if (pkg != null && !r.sbn.getPackageName().equals(pkg)) { - continue; - } - if (channelId != null && !channelId.equals(r.getChannel().getId())) { - continue; - } - if (canceledNotifications == null) { - canceledNotifications = new ArrayList<>(); - } - canceledNotifications.add(r); + // Why does this parameter exist? Do we actually want to execute the above if doit + // is false? if (!doit) { - return true; + return; } - mNotificationList.remove(i); - cancelNotificationLocked(r, false, reason); - } - mSnoozeHelper.cancel(userId, pkg); - if (doit && canceledNotifications != null) { - final int M = canceledNotifications.size(); - for (int i = 0; i < M; i++) { - cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, - listenerName, REASON_GROUP_SUMMARY_CANCELED, false /* sendDelete */); + + synchronized (mNotificationLock) { + FlagChecker flagChecker = (int flags) -> { + if ((flags & mustHaveFlags) != mustHaveFlags) { + return false; + } + if ((flags & mustNotHaveFlags) != 0) { + return false; + } + return true; + }; + + cancelAllNotificationsByListLocked(mNotificationList, callingUid, callingPid, + pkg, true /*nullPkgIndicatesUserSwitch*/, channelId, flagChecker, + false /*includeCurrentProfiles*/, userId, false /*sendDelete*/, reason, + listenerName); + cancelAllNotificationsByListLocked(mEnqueuedNotifications, callingUid, + callingPid, pkg, true /*nullPkgIndicatesUserSwitch*/, channelId, + flagChecker, false /*includeCurrentProfiles*/, userId, + false /*sendDelete*/, reason, listenerName); + mSnoozeHelper.cancel(userId, pkg); } } - if (canceledNotifications != null) { - updateLightsLocked(); + }); + } + + private interface FlagChecker { + // Returns false if these flags do not pass the defined flag test. + public boolean apply(int flags); + } + + private void cancelAllNotificationsByListLocked(ArrayList notificationList, + int callingUid, int callingPid, String pkg, boolean nullPkgIndicatesUserSwitch, + String channelId, FlagChecker flagChecker, boolean includeCurrentProfiles, int userId, + boolean sendDelete, int reason, String listenerName) { + ArrayList canceledNotifications = null; + for (int i = notificationList.size() - 1; i >= 0; --i) { + NotificationRecord r = notificationList.get(i); + if (includeCurrentProfiles) { + if (!notificationMatchesCurrentProfiles(r, userId)) { + continue; + } + } else if (!notificationMatchesUserId(r, userId)) { + continue; } - return canceledNotifications != null; + // Don't remove notifications to all, if there's no package name specified + if (nullPkgIndicatesUserSwitch && pkg == null && r.getUserId() == UserHandle.USER_ALL) { + continue; + } + if (!flagChecker.apply(r.getFlags())) { + continue; + } + if (pkg != null && !r.sbn.getPackageName().equals(pkg)) { + continue; + } + if (channelId != null && !channelId.equals(r.getChannel().getId())) { + continue; + } + + if (canceledNotifications == null) { + canceledNotifications = new ArrayList<>(); + } + canceledNotifications.add(r); + cancelNotificationLocked(r, sendDelete, reason); + } + if (canceledNotifications != null) { + final int M = canceledNotifications.size(); + for (int i = 0; i < M; i++) { + cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, + listenerName, false /* sendDelete */); + } + updateLightsLocked(); } } - void snoozeNotificationInt(String key, String snoozeCriterionId, ManagedServiceInfo listener) { + void snoozeNotificationInt(String key, long until, String snoozeCriterionId, + ManagedServiceInfo listener) { String listenerName = listener == null ? null : listener.component.toShortString(); // TODO: write to event log if (DBG) { - Slog.d(TAG, String.format("snooze event(%s, %s, %s)", - key, snoozeCriterionId, listenerName)); + Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, until, snoozeCriterionId, + listenerName)); } - synchronized (mNotificationList) { - final NotificationRecord r = mNotificationsByKey.get(key); - if (r != null) { - mNotificationList.remove(r); - cancelNotificationLocked(r, false, REASON_SNOOZED); - mNotificationAssistants.notifyAssistantSnoozedLocked(r.sbn, snoozeCriterionId); - updateLightsLocked(); - mSnoozeHelper.snooze(r); - savePolicyFile(); - } - } - } - - void snoozeNotificationInt(String key, long until, ManagedServiceInfo listener) { - String listenerName = listener == null ? null : listener.component.toShortString(); - // TODO: write to event log - if (DBG) { - Slog.d(TAG, String.format("snooze event(%s, %d, %s)", key, until, listenerName)); - } - if (until < System.currentTimeMillis()) { + if (until != SNOOZE_UNTIL_UNSPECIFIED && until < System.currentTimeMillis()) { return; } - synchronized (mNotificationList) { - final NotificationRecord r = mNotificationsByKey.get(key); - if (r != null) { - mNotificationList.remove(r); - cancelNotificationLocked(r, false, REASON_SNOOZED); - updateLightsLocked(); - mSnoozeHelper.snooze(r, until); - savePolicyFile(); + // Needs to post so that it can cancel notifications not yet enqueued. + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mNotificationLock) { + final NotificationRecord r = findNotificationByKeyLocked(key); + if (r != null) { + cancelNotificationLocked(r, false, REASON_SNOOZED); + updateLightsLocked(); + if (snoozeCriterionId != null) { + mNotificationAssistants.notifyAssistantSnoozedLocked(r.sbn, + snoozeCriterionId); + } + if (until == SNOOZE_UNTIL_UNSPECIFIED) { + mSnoozeHelper.snooze(r); + } else { + mSnoozeHelper.snooze(r, until); + } + savePolicyFile(); + } + } } - } - } - - void snoozeNotificationInt(String key, ManagedServiceInfo listener) { - String listenerName = listener == null ? null : listener.component.toShortString(); - // TODO: write to event log - if (DBG) { - Slog.d(TAG, String.format("snooze event(%s, %s)", key, listenerName)); - } - synchronized (mNotificationList) { - final NotificationRecord r = mNotificationsByKey.get(key); - if (r != null) { - mNotificationList.remove(r); - cancelNotificationLocked(r, false, REASON_SNOOZED); - updateLightsLocked(); - mSnoozeHelper.snooze(r); - savePolicyFile(); - } - } + }); } void unsnoozeNotificationInt(String key, ManagedServiceInfo listener) { @@ -3970,47 +3994,40 @@ public class NotificationManagerService extends SystemService { void cancelAllLocked(int callingUid, int callingPid, int userId, int reason, ManagedServiceInfo listener, boolean includeCurrentProfiles) { - String listenerName = listener == null ? null : listener.component.toShortString(); - EventLogTags.writeNotificationCancelAll(callingUid, callingPid, - null, userId, 0, 0, reason, listenerName); + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mNotificationLock) { + String listenerName = + listener == null ? null : listener.component.toShortString(); + EventLogTags.writeNotificationCancelAll(callingUid, callingPid, + null, userId, 0, 0, reason, listenerName); - ArrayList canceledNotifications = null; - final int N = mNotificationList.size(); - for (int i=N-1; i>=0; i--) { - NotificationRecord r = mNotificationList.get(i); - if (includeCurrentProfiles) { - if (!notificationMatchesCurrentProfiles(r, userId)) { - continue; - } - } else { - if (!notificationMatchesUserId(r, userId)) { - continue; + FlagChecker flagChecker = (int flags) -> { + if ((flags & (Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR)) + != 0) { + return false; + } + return true; + }; + + cancelAllNotificationsByListLocked(mNotificationList, callingUid, callingPid, + null, false /*nullPkgIndicatesUserSwitch*/, null, flagChecker, + includeCurrentProfiles, userId, true /*sendDelete*/, reason, + listenerName); + cancelAllNotificationsByListLocked(mEnqueuedNotifications, callingUid, + callingPid, null, false /*nullPkgIndicatesUserSwitch*/, null, + flagChecker, includeCurrentProfiles, userId, true /*sendDelete*/, + reason, listenerName); + mSnoozeHelper.cancel(userId, includeCurrentProfiles); } } - - if ((r.getFlags() & (Notification.FLAG_ONGOING_EVENT - | Notification.FLAG_NO_CLEAR)) == 0) { - mNotificationList.remove(i); - cancelNotificationLocked(r, true, reason); - // Make a note so we can cancel children later. - if (canceledNotifications == null) { - canceledNotifications = new ArrayList<>(); - } - canceledNotifications.add(r); - } - } - mSnoozeHelper.cancel(userId, includeCurrentProfiles); - int M = canceledNotifications != null ? canceledNotifications.size() : 0; - for (int i = 0; i < M; i++) { - cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, - listenerName, REASON_GROUP_SUMMARY_CANCELED, false /* sendDelete */); - } - updateLightsLocked(); + }); } // Warning: The caller is responsible for invoking updateLightsLocked(). private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid, - String listenerName, int reason, boolean sendDelete) { + String listenerName, boolean sendDelete) { Notification n = r.getNotification(); if (!n.isGroupSummary()) { return; @@ -4024,16 +4041,26 @@ public class NotificationManagerService extends SystemService { return; } - final int N = mNotificationList.size(); - for (int i = N - 1; i >= 0; i--) { - NotificationRecord childR = mNotificationList.get(i); - StatusBarNotification childSbn = childR.sbn; + cancelGroupChildrenByListLocked(mNotificationList, r, callingUid, callingPid, listenerName, + sendDelete); + cancelGroupChildrenByListLocked(mEnqueuedNotifications, r, callingUid, callingPid, + listenerName, sendDelete); + } + + private void cancelGroupChildrenByListLocked(ArrayList notificationList, + NotificationRecord parentNotification, int callingUid, int callingPid, + String listenerName, boolean sendDelete) { + final String pkg = parentNotification.sbn.getPackageName(); + final int userId = parentNotification.getUserId(); + final int reason = REASON_GROUP_SUMMARY_CANCELED; + for (int i = notificationList.size() - 1; i >= 0; i--) { + final NotificationRecord childR = notificationList.get(i); + final StatusBarNotification childSbn = childR.sbn; if ((childSbn.isGroup() && !childSbn.getNotification().isGroupSummary()) && - childR.getGroupKey().equals(r.getGroupKey()) + childR.getGroupKey().equals(parentNotification.getGroupKey()) && (childR.getFlags() & Notification.FLAG_FOREGROUND_SERVICE) == 0) { EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(), childSbn.getTag(), userId, 0, 0, reason, listenerName); - mNotificationList.remove(i); cancelNotificationLocked(childR, sendDelete, reason); } } @@ -4082,19 +4109,49 @@ public class NotificationManagerService extends SystemService { } } - // lock on mNotificationList - int indexOfNotificationLocked(String pkg, String tag, int id, int userId) + // Searches both enqueued and posted notifications by key. + // TODO: need to combine a bunch of these getters with slightly different behavior. + // TODO: Should enqueuing just add to mNotificationsByKey instead? + private NotificationRecord findNotificationByKeyLocked(String key) { + final int N = mNotificationList.size(); + for (int i = 0; i < N; i++) { + if (key.equals(mNotificationList.get(i).getKey())) { + return mNotificationList.get(i); + } + } + final int M = mEnqueuedNotifications.size(); + for (int i = 0; i < M; i++) { + if (key.equals(mEnqueuedNotifications.get(i).getKey())) { + return mEnqueuedNotifications.get(i); + } + } + return null; + } + + private NotificationRecord findNotificationLocked(String pkg, String tag, int id, int userId) { + NotificationRecord r; + if ((r = findNotificationByListLocked(mNotificationList, pkg, tag, id, userId)) != null) { + return r; + } + if ((r = findNotificationByListLocked(mEnqueuedNotifications, pkg, tag, id, userId)) + != null) { + return r; + } + return null; + } + + private NotificationRecord findNotificationByListLocked(ArrayList list, + String pkg, String tag, int id, int userId) { - ArrayList list = mNotificationList; final int len = list.size(); - for (int i=0; i + diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java index 40938fd88ef18..9b74fcc864e9d 100644 --- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,7 +24,9 @@ import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,18 +35,25 @@ import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.MessageQueue; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.support.test.annotation.UiThreadTest; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -58,30 +67,76 @@ import com.android.server.lights.LightsManager; @RunWith(AndroidJUnit4.class) public class NotificationManagerServiceTest { private final String pkg = "com.android.server.notification"; - private final int uid = 0; + private final int uid = Binder.getCallingUid(); private NotificationManagerService mNotificationManagerService; private INotificationManager mBinderService; private IPackageManager mPackageManager = mock(IPackageManager.class); + private Context mContext; + private HandlerThread mThread; @Before @UiThreadTest public void setUp() throws Exception { - final Context context = InstrumentationRegistry.getTargetContext(); - mNotificationManagerService = new NotificationManagerService(context); + mContext = InstrumentationRegistry.getTargetContext(); + mNotificationManagerService = new NotificationManagerService(mContext); // MockPackageManager - default returns ApplicationInfo with matching calling UID final ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.uid = Binder.getCallingUid(); + applicationInfo.uid = uid; when(mPackageManager.getApplicationInfo(any(), anyInt(), anyInt())) .thenReturn(applicationInfo); + final PackageManager mockPackageManagerClient = mock(PackageManager.class); + when(mockPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); final LightsManager mockLightsManager = mock(LightsManager.class); when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class)); - mNotificationManagerService.init(mPackageManager, mockLightsManager); + // Use a separate thread for service looper. + mThread = new HandlerThread("TestThread"); + mThread.start(); + // Mock NotificationListeners to bypass security checks. + final NotificationManagerService.NotificationListeners mockNotificationListeners = + mock(NotificationManagerService.NotificationListeners.class); + when(mockNotificationListeners.checkServiceTokenLocked(any())).thenReturn( + mockNotificationListeners.new ManagedServiceInfo(null, + new ComponentName(pkg, "test_class"), uid, true, null, 0)); + + mNotificationManagerService.init(mThread.getLooper(), mPackageManager, + mockPackageManagerClient, mockLightsManager, mockNotificationListeners); // Tests call directly into the Binder. mBinderService = mNotificationManagerService.getBinderService(); } + public void waitForIdle() throws Exception { + MessageQueue queue = mThread.getLooper().getQueue(); + CountDownLatch latch = new CountDownLatch(1); + queue.addIdleHandler(new MessageQueue.IdleHandler() { + @Override public boolean queueIdle() { + latch.countDown(); + return false; + } + }); + latch.await(); + if (!queue.isIdle()) { + waitForIdle(); + } + } + + private NotificationRecord generateNotificationRecord(NotificationChannel channel) { + if (channel == null) { + channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_DEFAULT); + } + Notification n = new Notification.Builder(mContext) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH) + .build(); + StatusBarNotification sbn = new StatusBarNotification(mContext.getPackageName(), + mContext.getPackageName(), channel, 1, "tag", uid, 0, + n, new UserHandle(uid), null, 0); + return new NotificationRecord(mContext, sbn); + } + @Test @UiThreadTest public void testCreateNotificationChannels_SingleChannel() throws Exception { @@ -203,15 +258,120 @@ public class NotificationManagerServiceTest { verify(usageStats, times(1)).registerBlocked(eq(r)); } - private NotificationRecord generateNotificationRecord(NotificationChannel channel) { - final Context context = InstrumentationRegistry.getTargetContext(); - Notification n = new Notification.Builder(context) - .setContentTitle("foo") - .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setPriority(Notification.PRIORITY_HIGH) - .build(); - StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, channel, 1, "tag", uid, uid, - n, UserHandle.SYSTEM, null, uid); - return new NotificationRecord(context, sbn); + @Test + @UiThreadTest + public void testEnqueueNotificationWithTag_PopulatesGetActiveNotifications() throws Exception { + mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0, + generateNotificationRecord(null).getNotification(), new int[1], 0); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(mContext.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception { + mBinderService.enqueueNotificationWithTag(mContext.getPackageName(), "opPkg", "tag", 0, + generateNotificationRecord(null).getNotification(), new int[1], 0); + mBinderService.cancelNotificationWithTag(mContext.getPackageName(), "tag", 0, 0); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(mContext.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelNotificationsFromListenerImmediatelyAfterEnqueue() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelNotificationsFromListener(null, null); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotificationsImmediatelyAfterEnqueue() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications(sbn.getPackageName(), sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_IgnoreForegroundService() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + sbn.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications(sbn.getPackageName(), sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_IgnoreOtherPackages() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + sbn.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications("other_pkg_name", sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_NullPkgRemovesAll() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.cancelAllNotifications(null, sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test + @UiThreadTest + public void testCancelAllNotifications_NullPkgIgnoresUserAllNotifications() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], UserHandle.USER_ALL); + // Null pkg is how we signal a user switch. + mBinderService.cancelAllNotifications(null, sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(1, notifs.length); + } + + @Test + @UiThreadTest + public void testSnoozeNotificationImmediatelyAfterEnqueue() throws Exception { + final StatusBarNotification sbn = generateNotificationRecord(null).sbn; + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), "opPkg", "tag", + sbn.getId(), sbn.getNotification(), new int[1], sbn.getUserId()); + mBinderService.snoozeNotificationFromListener(null, sbn.getKey()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); } }