Loading core/java/android/app/NotificationManager.java +64 −24 Original line number Original line Diff line number Diff line Loading @@ -68,6 +68,7 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.service.notification.ZenPolicy; import android.util.Log; import android.util.Log; import android.util.LruCache; import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.Retention; Loading Loading @@ -646,16 +647,21 @@ public class NotificationManager { */ */ public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500; public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500; private static final float MAX_NOTIFICATION_ENQUEUE_RATE = 5f; private static final float MAX_NOTIFICATION_UPDATE_RATE = 5f; private static final float MAX_NOTIFICATION_UNNECESSARY_CANCEL_RATE = 5f; private static final int KNOWN_STATUS_ENQUEUED = 1; private static final int KNOWN_STATUS_CANCELLED = 2; private final Context mContext; private final Context mContext; private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub> private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub> mCallNotificationEventCallbacks = new HashMap<>(); mCallNotificationEventCallbacks = new HashMap<>(); private final InstantSource mClock; private final InstantSource mClock; private final RateEstimator mEnqueueRateEstimator = new RateEstimator(); private final RateEstimator mUpdateRateEstimator = new RateEstimator(); private final LruCache<String, Boolean> mEnqueuedNotificationKeys = new LruCache<>(10); private final RateEstimator mUnnecessaryCancelRateEstimator = new RateEstimator(); private final Object mEnqueueThrottleLock = new Object(); // Value is KNOWN_STATUS_ENQUEUED/_CANCELLED private final LruCache<NotificationKey, Integer> mKnownNotifications = new LruCache<>(100); private final Object mThrottleLock = new Object(); @UnsupportedAppUsage @UnsupportedAppUsage private static INotificationManager sService; private static INotificationManager sService; Loading Loading @@ -780,7 +786,7 @@ public class NotificationManager { { { INotificationManager service = service(); INotificationManager service = service(); String pkg = mContext.getPackageName(); String pkg = mContext.getPackageName(); if (discardNotify(tag, id, notification)) { if (discardNotify(user, pkg, tag, id, notification)) { return; return; } } Loading @@ -797,32 +803,39 @@ public class NotificationManager { * Determines whether a {@link #notify} call should be skipped. If the notification is not * Determines whether a {@link #notify} call should be skipped. If the notification is not * skipped, updates tracking metadata to use in future decisions. * skipped, updates tracking metadata to use in future decisions. */ */ private boolean discardNotify(@Nullable String tag, int id, Notification notification) { private boolean discardNotify(UserHandle user, String pkg, @Nullable String tag, int id, Notification notification) { if (notificationClassification() if (notificationClassification() && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) { && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) { return true; return true; } } if (Flags.nmBinderPerfThrottleNotify()) { if (Flags.nmBinderPerfThrottleNotify()) { String key = toEnqueuedNotificationKey(tag, id); NotificationKey key = new NotificationKey(user, pkg, tag, id); long now = mClock.millis(); long now = mClock.millis(); synchronized (mEnqueueThrottleLock) { synchronized (mThrottleLock) { if (mEnqueuedNotificationKeys.get(key) != null Integer status = mKnownNotifications.get(key); && !notification.hasCompletedProgress() if (status != null && status == KNOWN_STATUS_ENQUEUED && mEnqueueRateEstimator.getRate(now) > MAX_NOTIFICATION_ENQUEUE_RATE) { && !notification.hasCompletedProgress()) { float updateRate = mUpdateRateEstimator.getRate(now); if (updateRate > MAX_NOTIFICATION_UPDATE_RATE) { Slog.w(TAG, "Shedding update of " + key + ", notification update maximum rate exceeded (" + updateRate + ")"); return true; return true; } } mUpdateRateEstimator.update(now); } mEnqueueRateEstimator.update(now); mKnownNotifications.put(key, KNOWN_STATUS_ENQUEUED); mEnqueuedNotificationKeys.put(key, Boolean.TRUE); } } } } return false; return false; } } private static String toEnqueuedNotificationKey(@Nullable String tag, int id) { return tag + "," + id; private record NotificationKey(@NonNull UserHandle user, @NonNull String pkg, } @Nullable String tag, int id) { } private Notification fixNotification(Notification notification) { private Notification fixNotification(Notification notification) { String pkg = mContext.getPackageName(); String pkg = mContext.getPackageName(); Loading Loading @@ -920,14 +933,12 @@ public class NotificationManager { @UnsupportedAppUsage @UnsupportedAppUsage public void cancelAsUser(@Nullable String tag, int id, UserHandle user) public void cancelAsUser(@Nullable String tag, int id, UserHandle user) { { if (Flags.nmBinderPerfThrottleNotify()) { String pkg = mContext.getPackageName(); synchronized (mEnqueueThrottleLock) { if (discardCancel(user, pkg, tag, id)) { mEnqueuedNotificationKeys.remove(toEnqueuedNotificationKey(tag, id)); return; } } } INotificationManager service = service(); INotificationManager service = service(); String pkg = mContext.getPackageName(); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); try { try { service.cancelNotificationWithTag( service.cancelNotificationWithTag( Loading @@ -937,6 +948,33 @@ public class NotificationManager { } } } } /** * Determines whether a {@link #cancel} call should be skipped. If not skipped, updates tracking * metadata to use in future decisions. */ private boolean discardCancel(UserHandle user, String pkg, @Nullable String tag, int id) { if (Flags.nmBinderPerfThrottleNotify()) { NotificationKey key = new NotificationKey(user, pkg, tag, id); long now = mClock.millis(); synchronized (mThrottleLock) { Integer status = mKnownNotifications.get(key); if (status != null && status == KNOWN_STATUS_CANCELLED) { float cancelRate = mUnnecessaryCancelRateEstimator.getRate(now); if (cancelRate > MAX_NOTIFICATION_UNNECESSARY_CANCEL_RATE) { Slog.w(TAG, "Shedding cancel of " + key + ", presumably unnecessary and maximum rate exceeded (" + cancelRate + ")"); return true; } mUnnecessaryCancelRateEstimator.update(now); } mKnownNotifications.put(key, KNOWN_STATUS_CANCELLED); } } return false; } /** /** * Cancel all previously shown notifications. See {@link #cancel} for the * Cancel all previously shown notifications. See {@link #cancel} for the * detailed behavior. * detailed behavior. Loading @@ -944,8 +982,10 @@ public class NotificationManager { public void cancelAll() public void cancelAll() { { if (Flags.nmBinderPerfThrottleNotify()) { if (Flags.nmBinderPerfThrottleNotify()) { synchronized (mEnqueueThrottleLock) { synchronized (mThrottleLock) { mEnqueuedNotificationKeys.evictAll(); for (NotificationKey key : mKnownNotifications.snapshot().keySet()) { mKnownNotifications.put(key, KNOWN_STATUS_CANCELLED); } } } } } Loading core/tests/coretests/src/android/app/NotificationManagerTest.java +52 −1 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.app; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.times; Loading Loading @@ -67,6 +68,8 @@ public class NotificationManagerTest { mClock.advanceByMillis(5); mClock.advanceByMillis(5); } } verify(mNotificationManager.mBackendService, atLeast(20)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(), verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); any(), any(), anyInt(), any(), anyInt()); } } Loading Loading @@ -101,7 +104,38 @@ public class NotificationManagerTest { @Test @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void notify_rapidAddAndCancel_isNotThrottled() throws Exception { public void cancel_unnecessaryAndRapid_isThrottled() throws Exception { for (int i = 0; i < 100; i++) { mNotificationManager.cancel(1); mClock.advanceByMillis(5); } verify(mNotificationManager.mBackendService, atLeast(20)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); verify(mNotificationManager.mBackendService, atMost(30)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void cancel_unnecessaryButReasonable_isNotThrottled() throws Exception { // Scenario: the app tries to repeatedly cancel a single notification, but at a reasonable // rate. Strange, but not what we're trying to block with NM_BINDER_PERF_THROTTLE_NOTIFY. for (int i = 0; i < 100; i++) { mNotificationManager.cancel(1); mClock.advanceByMillis(500); } verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void cancel_necessaryAndRapid_isNotThrottled() throws Exception { // Scenario: the app posts and immediately cancels a bunch of notifications. Strange, // but not what we're trying to block with NM_BINDER_PERF_THROTTLE_NOTIFY. Notification n = exampleNotification(); Notification n = exampleNotification(); for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) { Loading @@ -112,6 +146,23 @@ public class NotificationManagerTest { verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(), verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); any(), any(), anyInt(), any(), anyInt()); verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void cancel_maybeNecessaryAndRapid_isNotThrottled() throws Exception { // Scenario: the app posted a lot of notifications, is killed, then restarts (so NM client // doesn't know about them), then cancels them one by one. We don't want to throttle this // case. for (int i = 0; i < 100; i++) { mNotificationManager.cancel(i); mClock.advanceByMillis(1); } verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } } private Notification exampleNotification() { private Notification exampleNotification() { Loading Loading
core/java/android/app/NotificationManager.java +64 −24 Original line number Original line Diff line number Diff line Loading @@ -68,6 +68,7 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.service.notification.ZenPolicy; import android.util.Log; import android.util.Log; import android.util.LruCache; import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.Retention; Loading Loading @@ -646,16 +647,21 @@ public class NotificationManager { */ */ public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500; public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500; private static final float MAX_NOTIFICATION_ENQUEUE_RATE = 5f; private static final float MAX_NOTIFICATION_UPDATE_RATE = 5f; private static final float MAX_NOTIFICATION_UNNECESSARY_CANCEL_RATE = 5f; private static final int KNOWN_STATUS_ENQUEUED = 1; private static final int KNOWN_STATUS_CANCELLED = 2; private final Context mContext; private final Context mContext; private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub> private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub> mCallNotificationEventCallbacks = new HashMap<>(); mCallNotificationEventCallbacks = new HashMap<>(); private final InstantSource mClock; private final InstantSource mClock; private final RateEstimator mEnqueueRateEstimator = new RateEstimator(); private final RateEstimator mUpdateRateEstimator = new RateEstimator(); private final LruCache<String, Boolean> mEnqueuedNotificationKeys = new LruCache<>(10); private final RateEstimator mUnnecessaryCancelRateEstimator = new RateEstimator(); private final Object mEnqueueThrottleLock = new Object(); // Value is KNOWN_STATUS_ENQUEUED/_CANCELLED private final LruCache<NotificationKey, Integer> mKnownNotifications = new LruCache<>(100); private final Object mThrottleLock = new Object(); @UnsupportedAppUsage @UnsupportedAppUsage private static INotificationManager sService; private static INotificationManager sService; Loading Loading @@ -780,7 +786,7 @@ public class NotificationManager { { { INotificationManager service = service(); INotificationManager service = service(); String pkg = mContext.getPackageName(); String pkg = mContext.getPackageName(); if (discardNotify(tag, id, notification)) { if (discardNotify(user, pkg, tag, id, notification)) { return; return; } } Loading @@ -797,32 +803,39 @@ public class NotificationManager { * Determines whether a {@link #notify} call should be skipped. If the notification is not * Determines whether a {@link #notify} call should be skipped. If the notification is not * skipped, updates tracking metadata to use in future decisions. * skipped, updates tracking metadata to use in future decisions. */ */ private boolean discardNotify(@Nullable String tag, int id, Notification notification) { private boolean discardNotify(UserHandle user, String pkg, @Nullable String tag, int id, Notification notification) { if (notificationClassification() if (notificationClassification() && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) { && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) { return true; return true; } } if (Flags.nmBinderPerfThrottleNotify()) { if (Flags.nmBinderPerfThrottleNotify()) { String key = toEnqueuedNotificationKey(tag, id); NotificationKey key = new NotificationKey(user, pkg, tag, id); long now = mClock.millis(); long now = mClock.millis(); synchronized (mEnqueueThrottleLock) { synchronized (mThrottleLock) { if (mEnqueuedNotificationKeys.get(key) != null Integer status = mKnownNotifications.get(key); && !notification.hasCompletedProgress() if (status != null && status == KNOWN_STATUS_ENQUEUED && mEnqueueRateEstimator.getRate(now) > MAX_NOTIFICATION_ENQUEUE_RATE) { && !notification.hasCompletedProgress()) { float updateRate = mUpdateRateEstimator.getRate(now); if (updateRate > MAX_NOTIFICATION_UPDATE_RATE) { Slog.w(TAG, "Shedding update of " + key + ", notification update maximum rate exceeded (" + updateRate + ")"); return true; return true; } } mUpdateRateEstimator.update(now); } mEnqueueRateEstimator.update(now); mKnownNotifications.put(key, KNOWN_STATUS_ENQUEUED); mEnqueuedNotificationKeys.put(key, Boolean.TRUE); } } } } return false; return false; } } private static String toEnqueuedNotificationKey(@Nullable String tag, int id) { return tag + "," + id; private record NotificationKey(@NonNull UserHandle user, @NonNull String pkg, } @Nullable String tag, int id) { } private Notification fixNotification(Notification notification) { private Notification fixNotification(Notification notification) { String pkg = mContext.getPackageName(); String pkg = mContext.getPackageName(); Loading Loading @@ -920,14 +933,12 @@ public class NotificationManager { @UnsupportedAppUsage @UnsupportedAppUsage public void cancelAsUser(@Nullable String tag, int id, UserHandle user) public void cancelAsUser(@Nullable String tag, int id, UserHandle user) { { if (Flags.nmBinderPerfThrottleNotify()) { String pkg = mContext.getPackageName(); synchronized (mEnqueueThrottleLock) { if (discardCancel(user, pkg, tag, id)) { mEnqueuedNotificationKeys.remove(toEnqueuedNotificationKey(tag, id)); return; } } } INotificationManager service = service(); INotificationManager service = service(); String pkg = mContext.getPackageName(); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); try { try { service.cancelNotificationWithTag( service.cancelNotificationWithTag( Loading @@ -937,6 +948,33 @@ public class NotificationManager { } } } } /** * Determines whether a {@link #cancel} call should be skipped. If not skipped, updates tracking * metadata to use in future decisions. */ private boolean discardCancel(UserHandle user, String pkg, @Nullable String tag, int id) { if (Flags.nmBinderPerfThrottleNotify()) { NotificationKey key = new NotificationKey(user, pkg, tag, id); long now = mClock.millis(); synchronized (mThrottleLock) { Integer status = mKnownNotifications.get(key); if (status != null && status == KNOWN_STATUS_CANCELLED) { float cancelRate = mUnnecessaryCancelRateEstimator.getRate(now); if (cancelRate > MAX_NOTIFICATION_UNNECESSARY_CANCEL_RATE) { Slog.w(TAG, "Shedding cancel of " + key + ", presumably unnecessary and maximum rate exceeded (" + cancelRate + ")"); return true; } mUnnecessaryCancelRateEstimator.update(now); } mKnownNotifications.put(key, KNOWN_STATUS_CANCELLED); } } return false; } /** /** * Cancel all previously shown notifications. See {@link #cancel} for the * Cancel all previously shown notifications. See {@link #cancel} for the * detailed behavior. * detailed behavior. Loading @@ -944,8 +982,10 @@ public class NotificationManager { public void cancelAll() public void cancelAll() { { if (Flags.nmBinderPerfThrottleNotify()) { if (Flags.nmBinderPerfThrottleNotify()) { synchronized (mEnqueueThrottleLock) { synchronized (mThrottleLock) { mEnqueuedNotificationKeys.evictAll(); for (NotificationKey key : mKnownNotifications.snapshot().keySet()) { mKnownNotifications.put(key, KNOWN_STATUS_CANCELLED); } } } } } Loading
core/tests/coretests/src/android/app/NotificationManagerTest.java +52 −1 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.app; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.times; Loading Loading @@ -67,6 +68,8 @@ public class NotificationManagerTest { mClock.advanceByMillis(5); mClock.advanceByMillis(5); } } verify(mNotificationManager.mBackendService, atLeast(20)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(), verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); any(), any(), anyInt(), any(), anyInt()); } } Loading Loading @@ -101,7 +104,38 @@ public class NotificationManagerTest { @Test @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void notify_rapidAddAndCancel_isNotThrottled() throws Exception { public void cancel_unnecessaryAndRapid_isThrottled() throws Exception { for (int i = 0; i < 100; i++) { mNotificationManager.cancel(1); mClock.advanceByMillis(5); } verify(mNotificationManager.mBackendService, atLeast(20)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); verify(mNotificationManager.mBackendService, atMost(30)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void cancel_unnecessaryButReasonable_isNotThrottled() throws Exception { // Scenario: the app tries to repeatedly cancel a single notification, but at a reasonable // rate. Strange, but not what we're trying to block with NM_BINDER_PERF_THROTTLE_NOTIFY. for (int i = 0; i < 100; i++) { mNotificationManager.cancel(1); mClock.advanceByMillis(500); } verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void cancel_necessaryAndRapid_isNotThrottled() throws Exception { // Scenario: the app posts and immediately cancels a bunch of notifications. Strange, // but not what we're trying to block with NM_BINDER_PERF_THROTTLE_NOTIFY. Notification n = exampleNotification(); Notification n = exampleNotification(); for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) { Loading @@ -112,6 +146,23 @@ public class NotificationManagerTest { verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(), verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); any(), any(), anyInt(), any(), anyInt()); verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } @Test @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY) public void cancel_maybeNecessaryAndRapid_isNotThrottled() throws Exception { // Scenario: the app posted a lot of notifications, is killed, then restarts (so NM client // doesn't know about them), then cancels them one by one. We don't want to throttle this // case. for (int i = 0; i < 100; i++) { mNotificationManager.cancel(i); mClock.advanceByMillis(1); } verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(), any(), any(), anyInt(), anyInt()); } } private Notification exampleNotification() { private Notification exampleNotification() { Loading