From cf63ff1532e793560f62e1c75f3402b48b0f09ba Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 24 Jan 2017 13:55:48 -0500 Subject: [PATCH] Snoozing API changes - No one can snooze for an undetermined amount of time - Only the assistant can unsnooze - Listeners can retrieve a list of snoozed notifications Test: runtest systemui-notification, cts verifier Change-Id: Idfaee6d8bc15a5d41630f86f7e852468b07dc7d0 --- api/current.txt | 4 +- api/system-current.txt | 4 +- api/test-current.txt | 4 +- .../android/app/INotificationManager.aidl | 4 +- .../NotificationAssistantService.java | 18 +++ .../NotificationListenerService.java | 110 ++++++++---------- .../NotificationManagerService.java | 63 ++++++---- .../server/notification/SnoozeHelper.java | 21 ++++ .../NotificationManagerServiceTest.java | 13 --- .../server/notification/SnoozeHelperTest.java | 35 ++++++ 10 files changed, 169 insertions(+), 107 deletions(-) diff --git a/api/current.txt b/api/current.txt index 42242a7b0cfa7..ad28b94fa1a79 100644 --- a/api/current.txt +++ b/api/current.txt @@ -35814,6 +35814,7 @@ package android.service.notification { method public final android.os.IBinder onBind(android.content.Intent); method public abstract android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean); method public abstract void onNotificationSnoozedUntilContext(android.service.notification.StatusBarNotification, java.lang.String); + method public final void unsnoozeNotification(java.lang.String); method public void updateNotificationChannel(java.lang.String, android.app.NotificationChannel); field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService"; } @@ -35829,6 +35830,7 @@ package android.service.notification { method public final int getCurrentInterruptionFilter(); method public final int getCurrentListenerHints(); method public android.service.notification.NotificationListenerService.RankingMap getCurrentRanking(); + method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications(); method public android.os.IBinder onBind(android.content.Intent); method public void onInterruptionFilterChanged(int); method public void onListenerConnected(); @@ -35847,8 +35849,6 @@ package android.service.notification { method public final void setNotificationsShown(java.lang.String[]); method public final void snoozeNotification(java.lang.String, java.lang.String); method public final void snoozeNotification(java.lang.String, long); - method public final void snoozeNotification(java.lang.String); - method public final void unsnoozeNotification(java.lang.String); field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4 field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1 field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2 diff --git a/api/system-current.txt b/api/system-current.txt index c159938ebdc50..b9e89f61e5e02 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -38797,6 +38797,7 @@ package android.service.notification { method public final android.os.IBinder onBind(android.content.Intent); method public abstract android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean); method public abstract void onNotificationSnoozedUntilContext(android.service.notification.StatusBarNotification, java.lang.String); + method public final void unsnoozeNotification(java.lang.String); method public void updateNotificationChannel(java.lang.String, android.app.NotificationChannel); field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService"; } @@ -38814,6 +38815,7 @@ package android.service.notification { method public final int getCurrentInterruptionFilter(); method public final int getCurrentListenerHints(); method public android.service.notification.NotificationListenerService.RankingMap getCurrentRanking(); + method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications(); method public android.os.IBinder onBind(android.content.Intent); method public void onInterruptionFilterChanged(int); method public void onListenerConnected(); @@ -38834,9 +38836,7 @@ package android.service.notification { method public final void setOnNotificationPostedTrim(int); method public final void snoozeNotification(java.lang.String, java.lang.String); method public final void snoozeNotification(java.lang.String, long); - method public final void snoozeNotification(java.lang.String); method public void unregisterAsSystemService() throws android.os.RemoteException; - method public final void unsnoozeNotification(java.lang.String); field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4 field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1 field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2 diff --git a/api/test-current.txt b/api/test-current.txt index cc17d7f04007a..0e6548e3c2e64 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -35935,6 +35935,7 @@ package android.service.notification { method public final android.os.IBinder onBind(android.content.Intent); method public abstract android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean); method public abstract void onNotificationSnoozedUntilContext(android.service.notification.StatusBarNotification, java.lang.String); + method public final void unsnoozeNotification(java.lang.String); method public void updateNotificationChannel(java.lang.String, android.app.NotificationChannel); field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService"; } @@ -35950,6 +35951,7 @@ package android.service.notification { method public final int getCurrentInterruptionFilter(); method public final int getCurrentListenerHints(); method public android.service.notification.NotificationListenerService.RankingMap getCurrentRanking(); + method public final android.service.notification.StatusBarNotification[] getSnoozedNotifications(); method public android.os.IBinder onBind(android.content.Intent); method public void onInterruptionFilterChanged(int); method public void onListenerConnected(); @@ -35968,8 +35970,6 @@ package android.service.notification { method public final void setNotificationsShown(java.lang.String[]); method public final void snoozeNotification(java.lang.String, java.lang.String); method public final void snoozeNotification(java.lang.String, long); - method public final void snoozeNotification(java.lang.String); - method public final void unsnoozeNotification(java.lang.String); field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4 field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1 field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2 diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index d674bfe232315..740af9cb69de6 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -75,8 +75,6 @@ interface INotificationManager void snoozeNotificationUntilContextFromListener(in INotificationListener token, String key, String snoozeCriterionId); void snoozeNotificationUntilFromListener(in INotificationListener token, String key, long until); - void snoozeNotificationFromListener(in INotificationListener token, String key); - void unsnoozeNotificationFromListener(in INotificationListener token, String key); void requestBindListener(in ComponentName component); void requestUnbindListener(in INotificationListener token); @@ -86,6 +84,7 @@ interface INotificationManager void setNotificationsShownFromListener(in INotificationListener token, in String[] keys); ParceledListSlice getActiveNotificationsFromListener(in INotificationListener token, in String[] keys, int trim); + ParceledListSlice getSnoozedNotificationsFromListener(in INotificationListener token, int trim); void requestHintsFromListener(in INotificationListener token, int hints); int getHintsFromListener(in INotificationListener token); void requestInterruptionFilterFromListener(in INotificationListener token, int interruptionFilter); @@ -100,6 +99,7 @@ interface INotificationManager void updateNotificationChannelFromAssistant(in INotificationListener token, String pkg, in NotificationChannel channel); void deleteNotificationChannelFromAssistant(in INotificationListener token, String pkg, String channelId); ParceledListSlice getNotificationChannelsFromAssistant(in INotificationListener token, String pkg); + void unsnoozeNotificationFromAssistant(in INotificationListener token, String key); ComponentName getEffectsSuppressor(); boolean matchesCallFilter(in Bundle extras); diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java index d7a02a8042a19..cecdbee75b00d 100644 --- a/core/java/android/service/notification/NotificationAssistantService.java +++ b/core/java/android/service/notification/NotificationAssistantService.java @@ -118,6 +118,24 @@ public abstract class NotificationAssistantService extends NotificationListenerS } } + /** + * Inform the notification manager about un-snoozing a specific notification. + *

+ * This should only be used for notifications snoozed by this listener using + * {@link #snoozeNotification(String, String)}. Once un-snoozed, you will get a + * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the + * notification. + * @param key The key of the notification to snooze + */ + public final void unsnoozeNotification(String key) { + if (!isBound()) return; + try { + getNotificationInterface().unsnoozeNotificationFromAssistant(mWrapper, key); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact notification manager", ex); + } + } + /** * Creates a notification channel that notifications can be posted to for a given package. * diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index d9306897d8767..517b305e0a161 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -562,43 +562,6 @@ public abstract class NotificationListenerService extends Service { } } - /** - * Inform the notification manager about snoozing a specific notification. - *

- * Use this to snooze a notification for an indeterminate time. Upon being informed, the - * notification manager will actually remove the notification and you will get an - * {@link #onNotificationRemoved(StatusBarNotification)} callback. When the - * snoozing period expires, you will get a - * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the - * notification. Use {@link #unsnoozeNotification(String)} to restore the notification. - * @param key The key of the notification to snooze - */ - public final void snoozeNotification(String key) { - if (!isBound()) return; - try { - getNotificationInterface().snoozeNotificationFromListener(mWrapper, key); - } catch (android.os.RemoteException ex) { - Log.v(TAG, "Unable to contact notification manager", ex); - } - } - - /** - * Inform the notification manager about un-snoozing a specific notification. - *

- * This should only be used for notifications snoozed by this listener using - * {@link #snoozeNotification(String)}. Once un-snoozed, you will get a - * {@link #onNotificationPosted(StatusBarNotification, RankingMap)} callback for the - * notification. - * @param key The key of the notification to snooze - */ - public final void unsnoozeNotification(String key) { - if (!isBound()) return; - try { - getNotificationInterface().unsnoozeNotificationFromListener(mWrapper, key); - } catch (android.os.RemoteException ex) { - Log.v(TAG, "Unable to contact notification manager", ex); - } - } /** * Inform the notification manager that these notifications have been viewed by the @@ -662,6 +625,26 @@ public abstract class NotificationListenerService extends Service { return getActiveNotifications(null, TRIM_FULL); } + /** + * Like {@link #getActiveNotifications()}, but returns the list of currently snoozed + * notifications, for all users this listener has access to. + * + *

The service should wait for the {@link #onListenerConnected()} event + * before performing this operation. + * + * @return An array of active notifications, sorted in natural order. + */ + public final StatusBarNotification[] getSnoozedNotifications() { + try { + ParceledListSlice parceledList = getNotificationInterface() + .getSnoozedNotificationsFromListener(mWrapper, TRIM_FULL); + return cleanUpNotificationList(parceledList); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact notification manager", ex); + } + return null; + } + /** * Request the list of outstanding notifications (that is, those that are visible to the * current user). Useful when you don't know what's already been posted. @@ -711,36 +694,41 @@ public abstract class NotificationListenerService extends Service { try { ParceledListSlice parceledList = getNotificationInterface() .getActiveNotificationsFromListener(mWrapper, keys, trim); - List list = parceledList.getList(); - ArrayList corruptNotifications = null; - int N = list.size(); - for (int i = 0; i < N; i++) { - StatusBarNotification sbn = list.get(i); - Notification notification = sbn.getNotification(); - try { - // convert icon metadata to legacy format for older clients - createLegacyIconExtras(notification); - // populate remote views for older clients. - maybePopulateRemoteViews(notification); - } catch (IllegalArgumentException e) { - if (corruptNotifications == null) { - corruptNotifications = new ArrayList<>(N); - } - corruptNotifications.add(sbn); - Log.w(TAG, "onNotificationPosted: can't rebuild notification from " + - sbn.getPackageName()); - } - } - if (corruptNotifications != null) { - list.removeAll(corruptNotifications); - } - return list.toArray(new StatusBarNotification[list.size()]); + return cleanUpNotificationList(parceledList); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); } return null; } + private StatusBarNotification[] cleanUpNotificationList( + ParceledListSlice parceledList) { + List list = parceledList.getList(); + ArrayList corruptNotifications = null; + int N = list.size(); + for (int i = 0; i < N; i++) { + StatusBarNotification sbn = list.get(i); + Notification notification = sbn.getNotification(); + try { + // convert icon metadata to legacy format for older clients + createLegacyIconExtras(notification); + // populate remote views for older clients. + maybePopulateRemoteViews(notification); + } catch (IllegalArgumentException e) { + if (corruptNotifications == null) { + corruptNotifications = new ArrayList<>(N); + } + corruptNotifications.add(sbn); + Log.w(TAG, "get(Active/Snoozed)Notifications: can't rebuild notification from " + + sbn.getPackageName()); + } + } + if (corruptNotifications != null) { + list.removeAll(corruptNotifications); + } + return list.toArray(new StatusBarNotification[list.size()]); + } + /** * Gets the set of hints representing current state. * diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 96459be9234ab..72faa81ab2498 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1944,31 +1944,16 @@ public class NotificationManagerService extends SystemService { } /** - * Allow an INotificationListener to snooze a single notification. + * Allows the notification assistant to un-snooze a single notification. * - * @param token The binder for the listener, to check that the caller is allowed + * @param token The binder for the assistant, to check that the caller is allowed */ @Override - public void snoozeNotificationFromListener(INotificationListener token, String key) { + public void unsnoozeNotificationFromAssistant(INotificationListener token, String key) { long identity = Binder.clearCallingIdentity(); try { - final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - snoozeNotificationInt(key, SNOOZE_UNTIL_UNSPECIFIED, null, info); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - /** - * Allow an INotificationListener to un-snooze a single notification. - * - * @param token The binder for the listener, to check that the caller is allowed - */ - @Override - public void unsnoozeNotificationFromListener(INotificationListener token, String key) { - long identity = Binder.clearCallingIdentity(); - try { - final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); + final ManagedServiceInfo info = + mNotificationAssistants.checkServiceTokenLocked(token); unsnoozeNotificationInt(key, info); } finally { Binder.restoreCallingIdentity(identity); @@ -2039,6 +2024,36 @@ public class NotificationManagerService extends SystemService { } } + /** + * Allow an INotificationListener to request the list of outstanding snoozed notifications + * seen by the current user. Useful when starting up, after which point the listener + * callbacks should be used. + * + * @param token The binder for the listener, to check that the caller is allowed + * @returns The return value will contain the notifications specified in keys, in that + * order, or if keys is null, all the notifications, in natural order. + */ + @Override + public ParceledListSlice getSnoozedNotificationsFromListener( + INotificationListener token, int trim) { + synchronized (mNotificationLock) { + final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); + List snoozedRecords = mSnoozeHelper.getSnoozed(); + final int N = snoozedRecords.size(); + final ArrayList list = new ArrayList<>(N); + for (int i=0; i < N; i++) { + final NotificationRecord r = snoozedRecords.get(i); + if (r == null) continue; + StatusBarNotification sbn = r.sbn; + if (!isVisibleToListener(sbn, info)) continue; + StatusBarNotification sbnToSend = + (trim == TRIM_FULL) ? sbn : sbn.cloneLight(); + list.add(sbnToSend); + } + return new ParceledListSlice<>(list); + } + } + @Override public void requestHintsFromListener(INotificationListener token, int hints) { final long identity = Binder.clearCallingIdentity(); @@ -3982,14 +3997,14 @@ public class NotificationManagerService extends SystemService { void snoozeNotificationInt(String key, long until, String snoozeCriterionId, ManagedServiceInfo listener) { String listenerName = listener == null ? null : listener.component.toShortString(); + if (until < System.currentTimeMillis() && snoozeCriterionId == null) { + return; + } // TODO: write to event log if (DBG) { Slog.d(TAG, String.format("snooze event(%s, %d, %s, %s)", key, until, snoozeCriterionId, listenerName)); } - if (until != SNOOZE_UNTIL_UNSPECIFIED && until < System.currentTimeMillis()) { - return; - } // Needs to post so that it can cancel notifications not yet enqueued. mHandler.post(new Runnable() { @Override @@ -4002,8 +4017,6 @@ public class NotificationManagerService extends SystemService { if (snoozeCriterionId != null) { mNotificationAssistants.notifyAssistantSnoozedLocked(r.sbn, snoozeCriterionId); - } - if (until == SNOOZE_UNTIL_UNSPECIFIED) { mSnoozeHelper.snooze(r); } else { mSnoozeHelper.snooze(r, until); diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java index e14700a995128..f2aff1102b0bb 100644 --- a/services/core/java/com/android/server/notification/SnoozeHelper.java +++ b/services/core/java/com/android/server/notification/SnoozeHelper.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -98,6 +99,26 @@ public class SnoozeHelper { return Collections.EMPTY_LIST; } + protected List getSnoozed() { + List snoozedForUser = new ArrayList<>(); + int[] userIds = mUserProfiles.getCurrentProfileIds(); + final int N = userIds.length; + for (int i = 0; i < N; i++) { + final ArrayMap> snoozedPkgs = + mSnoozedNotifications.get(userIds[i]); + if (snoozedPkgs != null) { + final int M = snoozedPkgs.size(); + for (int j = 0; j < M; j++) { + final ArrayMap records = snoozedPkgs.valueAt(j); + if (records != null) { + snoozedForUser.addAll(records.values()); + } + } + } + } + return snoozedForUser; + } + /** * Snoozes a notification and schedules an alarm to repost at that time. */ 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 92d9810f7c3f7..250aab871049e 100644 --- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -361,17 +361,4 @@ public class NotificationManagerServiceTest { 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); - } } diff --git a/services/tests/notification/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/notification/src/com/android/server/notification/SnoozeHelperTest.java index b7931d4f6a01f..69724f48a1116 100644 --- a/services/tests/notification/src/com/android/server/notification/SnoozeHelperTest.java +++ b/services/tests/notification/src/com/android/server/notification/SnoozeHelperTest.java @@ -33,6 +33,7 @@ import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.Matchers.any; @@ -42,6 +43,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @SmallTest @@ -190,6 +192,39 @@ public class SnoozeHelperTest { verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r); } + @Test + public void testGetSnoozedByUser() throws Exception { + NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM); + NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM); + NotificationRecord r3 = getNotificationRecord("pkg2", 3, "three", UserHandle.SYSTEM); + NotificationRecord r4 = getNotificationRecord("pkg2", 3, "three", UserHandle.CURRENT); + mSnoozeHelper.snooze(r, 1000); + mSnoozeHelper.snooze(r2, 1000); + mSnoozeHelper.snooze(r3, 1000); + mSnoozeHelper.snooze(r4, 1000); + when(mUserProfiles.getCurrentProfileIds()).thenReturn( + new int[] {UserHandle.USER_SYSTEM}); + assertEquals(3, mSnoozeHelper.getSnoozed().size()); + when(mUserProfiles.getCurrentProfileIds()).thenReturn( + new int[] {UserHandle.USER_CURRENT}); + assertEquals(1, mSnoozeHelper.getSnoozed().size()); + } + + @Test + public void testGetSnoozedByUser_managedProfiles() throws Exception { + when(mUserProfiles.getCurrentProfileIds()).thenReturn( + new int[] {UserHandle.USER_SYSTEM, UserHandle.USER_CURRENT}); + NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM); + NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM); + NotificationRecord r3 = getNotificationRecord("pkg2", 3, "three", UserHandle.SYSTEM); + NotificationRecord r4 = getNotificationRecord("pkg2", 3, "three", UserHandle.CURRENT); + mSnoozeHelper.snooze(r, 1000); + mSnoozeHelper.snooze(r2, 1000); + mSnoozeHelper.snooze(r3, 1000); + mSnoozeHelper.snooze(r4, 1000); + assertEquals(4, mSnoozeHelper.getSnoozed().size()); + } + private NotificationRecord getNotificationRecord(String pkg, int id, String tag, UserHandle user) { Notification n = new Notification.Builder(getContext())