Loading core/java/android/app/NotificationManager.java +64 −24 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.util.Log; import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; Loading Loading @@ -646,16 +647,21 @@ public class NotificationManager { */ 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 Map<CallNotificationEventListener, CallNotificationEventCallbackStub> mCallNotificationEventCallbacks = new HashMap<>(); private final InstantSource mClock; private final RateEstimator mEnqueueRateEstimator = new RateEstimator(); private final LruCache<String, Boolean> mEnqueuedNotificationKeys = new LruCache<>(10); private final Object mEnqueueThrottleLock = new Object(); private final RateEstimator mUpdateRateEstimator = new RateEstimator(); private final RateEstimator mUnnecessaryCancelRateEstimator = new RateEstimator(); // Value is KNOWN_STATUS_ENQUEUED/_CANCELLED private final LruCache<NotificationKey, Integer> mKnownNotifications = new LruCache<>(100); private final Object mThrottleLock = new Object(); @UnsupportedAppUsage private static INotificationManager sService; Loading Loading @@ -780,7 +786,7 @@ public class NotificationManager { { INotificationManager service = service(); String pkg = mContext.getPackageName(); if (discardNotify(tag, id, notification)) { if (discardNotify(user, pkg, tag, id, notification)) { return; } Loading @@ -797,32 +803,39 @@ public class NotificationManager { * Determines whether a {@link #notify} call should be skipped. If the notification is not * 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() && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) { return true; } if (Flags.nmBinderPerfThrottleNotify()) { String key = toEnqueuedNotificationKey(tag, id); NotificationKey key = new NotificationKey(user, pkg, tag, id); long now = mClock.millis(); synchronized (mEnqueueThrottleLock) { if (mEnqueuedNotificationKeys.get(key) != null && !notification.hasCompletedProgress() && mEnqueueRateEstimator.getRate(now) > MAX_NOTIFICATION_ENQUEUE_RATE) { synchronized (mThrottleLock) { Integer status = mKnownNotifications.get(key); if (status != null && status == KNOWN_STATUS_ENQUEUED && !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; } mUpdateRateEstimator.update(now); } mEnqueueRateEstimator.update(now); mEnqueuedNotificationKeys.put(key, Boolean.TRUE); mKnownNotifications.put(key, KNOWN_STATUS_ENQUEUED); } } 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) { String pkg = mContext.getPackageName(); Loading Loading @@ -920,14 +933,12 @@ public class NotificationManager { @UnsupportedAppUsage public void cancelAsUser(@Nullable String tag, int id, UserHandle user) { if (Flags.nmBinderPerfThrottleNotify()) { synchronized (mEnqueueThrottleLock) { mEnqueuedNotificationKeys.remove(toEnqueuedNotificationKey(tag, id)); } String pkg = mContext.getPackageName(); if (discardCancel(user, pkg, tag, id)) { return; } INotificationManager service = service(); String pkg = mContext.getPackageName(); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); try { 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 * detailed behavior. Loading @@ -944,8 +982,10 @@ public class NotificationManager { public void cancelAll() { if (Flags.nmBinderPerfThrottleNotify()) { synchronized (mEnqueueThrottleLock) { mEnqueuedNotificationKeys.evictAll(); synchronized (mThrottleLock) { 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 Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.app; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; Loading Loading @@ -67,6 +68,8 @@ public class NotificationManagerTest { mClock.advanceByMillis(5); } verify(mNotificationManager.mBackendService, atLeast(20)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); } Loading Loading @@ -101,7 +104,38 @@ public class NotificationManagerTest { @Test @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(); for (int i = 0; i < 100; i++) { Loading @@ -112,6 +146,23 @@ public class NotificationManagerTest { verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(), 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() { Loading Loading
core/java/android/app/NotificationManager.java +64 −24 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.util.Log; import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; import java.lang.annotation.Retention; Loading Loading @@ -646,16 +647,21 @@ public class NotificationManager { */ 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 Map<CallNotificationEventListener, CallNotificationEventCallbackStub> mCallNotificationEventCallbacks = new HashMap<>(); private final InstantSource mClock; private final RateEstimator mEnqueueRateEstimator = new RateEstimator(); private final LruCache<String, Boolean> mEnqueuedNotificationKeys = new LruCache<>(10); private final Object mEnqueueThrottleLock = new Object(); private final RateEstimator mUpdateRateEstimator = new RateEstimator(); private final RateEstimator mUnnecessaryCancelRateEstimator = new RateEstimator(); // Value is KNOWN_STATUS_ENQUEUED/_CANCELLED private final LruCache<NotificationKey, Integer> mKnownNotifications = new LruCache<>(100); private final Object mThrottleLock = new Object(); @UnsupportedAppUsage private static INotificationManager sService; Loading Loading @@ -780,7 +786,7 @@ public class NotificationManager { { INotificationManager service = service(); String pkg = mContext.getPackageName(); if (discardNotify(tag, id, notification)) { if (discardNotify(user, pkg, tag, id, notification)) { return; } Loading @@ -797,32 +803,39 @@ public class NotificationManager { * Determines whether a {@link #notify} call should be skipped. If the notification is not * 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() && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) { return true; } if (Flags.nmBinderPerfThrottleNotify()) { String key = toEnqueuedNotificationKey(tag, id); NotificationKey key = new NotificationKey(user, pkg, tag, id); long now = mClock.millis(); synchronized (mEnqueueThrottleLock) { if (mEnqueuedNotificationKeys.get(key) != null && !notification.hasCompletedProgress() && mEnqueueRateEstimator.getRate(now) > MAX_NOTIFICATION_ENQUEUE_RATE) { synchronized (mThrottleLock) { Integer status = mKnownNotifications.get(key); if (status != null && status == KNOWN_STATUS_ENQUEUED && !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; } mUpdateRateEstimator.update(now); } mEnqueueRateEstimator.update(now); mEnqueuedNotificationKeys.put(key, Boolean.TRUE); mKnownNotifications.put(key, KNOWN_STATUS_ENQUEUED); } } 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) { String pkg = mContext.getPackageName(); Loading Loading @@ -920,14 +933,12 @@ public class NotificationManager { @UnsupportedAppUsage public void cancelAsUser(@Nullable String tag, int id, UserHandle user) { if (Flags.nmBinderPerfThrottleNotify()) { synchronized (mEnqueueThrottleLock) { mEnqueuedNotificationKeys.remove(toEnqueuedNotificationKey(tag, id)); } String pkg = mContext.getPackageName(); if (discardCancel(user, pkg, tag, id)) { return; } INotificationManager service = service(); String pkg = mContext.getPackageName(); if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")"); try { 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 * detailed behavior. Loading @@ -944,8 +982,10 @@ public class NotificationManager { public void cancelAll() { if (Flags.nmBinderPerfThrottleNotify()) { synchronized (mEnqueueThrottleLock) { mEnqueuedNotificationKeys.evictAll(); synchronized (mThrottleLock) { 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 Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.app; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; Loading Loading @@ -67,6 +68,8 @@ public class NotificationManagerTest { mClock.advanceByMillis(5); } verify(mNotificationManager.mBackendService, atLeast(20)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(), any(), any(), anyInt(), any(), anyInt()); } Loading Loading @@ -101,7 +104,38 @@ public class NotificationManagerTest { @Test @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(); for (int i = 0; i < 100; i++) { Loading @@ -112,6 +146,23 @@ public class NotificationManagerTest { verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(), 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() { Loading