Loading core/java/android/app/Notification.java +16 −0 Original line number Diff line number Diff line Loading @@ -2543,6 +2543,22 @@ public class Notification implements Parcelable } } /** * @hide */ public boolean hasCompletedProgress() { // not a progress notification; can't be complete if (!extras.containsKey(EXTRA_PROGRESS) || !extras.containsKey(EXTRA_PROGRESS_MAX)) { return false; } // many apps use max 0 for 'indeterminate'; not complete if (extras.getInt(EXTRA_PROGRESS_MAX) == 0) { return false; } return extras.getInt(EXTRA_PROGRESS) == extras.getInt(EXTRA_PROGRESS_MAX); } /** @removed */ @Deprecated public String getChannel() { Loading core/tests/coretests/src/android/app/NotificationTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -99,6 +99,45 @@ public class NotificationTest { assertTrue(satisfiesTextContrast(secondaryTextColor, backgroundColor)); } @Test public void testHasCompletedProgress_noProgress() { Notification n = new Notification.Builder(mContext).build(); assertFalse(n.hasCompletedProgress()); } @Test public void testHasCompletedProgress_complete() { Notification n = new Notification.Builder(mContext) .setProgress(100, 100, true) .build(); Notification n2 = new Notification.Builder(mContext) .setProgress(10, 10, false) .build(); assertTrue(n.hasCompletedProgress()); assertTrue(n2.hasCompletedProgress()); } @Test public void testHasCompletedProgress_notComplete() { Notification n = new Notification.Builder(mContext) .setProgress(100, 99, true) .build(); Notification n2 = new Notification.Builder(mContext) .setProgress(10, 4, false) .build(); assertFalse(n.hasCompletedProgress()); assertFalse(n2.hasCompletedProgress()); } @Test public void testHasCompletedProgress_zeroMax() { Notification n = new Notification.Builder(mContext) .setProgress(0, 0, true) .build(); assertFalse(n.hasCompletedProgress()); } private Notification.Builder getMediaNotification() { MediaSession session = new MediaSession(mContext, "test"); return new Notification.Builder(mContext, "color") Loading services/core/java/com/android/server/notification/NotificationManagerService.java +47 −28 Original line number Diff line number Diff line Loading @@ -211,7 +211,7 @@ public class NotificationManagerService extends SystemService { = SystemProperties.getBoolean("debug.child_notifs", true); static final int MAX_PACKAGE_NOTIFICATIONS = 50; static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 10f; static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f; // message codes static final int MESSAGE_TIMEOUT = 2; Loading Loading @@ -3472,8 +3472,19 @@ public class NotificationManagerService extends SystemService { // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { synchronized (mNotificationLock) { if (mNotificationsByKey.get(r.sbn.getKey()) != null) { // this is an update, rate limit updates only if (mNotificationsByKey.get(r.sbn.getKey()) == null && isCallerInstantApp(pkg)) { // Ephemeral apps have some special constraints for notifications. // They are not allowed to create new notifications however they are allowed to // update notifications created by the system (e.g. a foreground service // notification). throw new SecurityException("Instant app " + pkg + " cannot create notifications"); } // rate limit updates that aren't completed progress notifications if (mNotificationsByKey.get(r.sbn.getKey()) != null && !r.getNotification().hasCompletedProgress()) { final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg); if (appEnqueueRate > mMaxPackageEnqueueRate) { mUsageStats.registerOverRateQuota(pkg); Loading @@ -3485,36 +3496,18 @@ public class NotificationManagerService extends SystemService { } return false; } } else if (isCallerInstantApp(pkg)) { // Ephemeral apps have some special constraints for notifications. // They are not allowed to create new notifications however they are allowed to // update notifications created by the system (e.g. a foreground service // notification). throw new SecurityException("Instant app " + pkg + " cannot create notifications"); } int count = 0; final int N = mNotificationList.size(); for (int i=0; i<N; i++) { final NotificationRecord existing = mNotificationList.get(i); if (existing.sbn.getPackageName().equals(pkg) && existing.sbn.getUserId() == userId) { if (existing.sbn.getId() == id && TextUtils.equals(existing.sbn.getTag(), tag)) { break; // Allow updating existing notification } count++; // limit the number of outstanding notificationrecords an app can have int count = getNotificationCountLocked(pkg, userId, id, tag); if (count >= MAX_PACKAGE_NOTIFICATIONS) { mUsageStats.registerOverCountQuota(pkg); Slog.e(TAG, "Package has already posted " + count Slog.e(TAG, "Package has already posted or enqueued " + count + " notifications. Not showing more. package=" + pkg); return false; } } } } } // snoozed apps if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) { Loading @@ -3538,6 +3531,32 @@ public class NotificationManagerService extends SystemService { return true; } protected int getNotificationCountLocked(String pkg, int userId, int excludedId, String excludedTag) { int count = 0; final int N = mNotificationList.size(); for (int i = 0; i < N; i++) { final NotificationRecord existing = mNotificationList.get(i); if (existing.sbn.getPackageName().equals(pkg) && existing.sbn.getUserId() == userId) { if (existing.sbn.getId() == excludedId && TextUtils.equals(existing.sbn.getTag(), excludedTag)) { continue; } count++; } } final int M = mEnqueuedNotifications.size(); for (int i = 0; i < M; i++) { final NotificationRecord existing = mEnqueuedNotifications.get(i); if (existing.sbn.getPackageName().equals(pkg) && existing.sbn.getUserId() == userId) { count++; } } return count; } protected boolean isBlocked(NotificationRecord r, NotificationUsageStats usageStats) { final String pkg = r.sbn.getPackageName(); final int callingUid = r.sbn.getUid(); Loading services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java +33 −0 Original line number Diff line number Diff line Loading @@ -1242,4 +1242,37 @@ public class NotificationManagerServiceTest extends NotificationTestCase { assertFalse(posted.getNotification().isColorized()); } @Test public void testGetNotificationCountLocked() throws Exception { for (int i = 0; i < 20; i++) { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, i, null, false); mNotificationManagerService.addEnqueuedNotification(r); } for (int i = 0; i < 20; i++) { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, i, null, false); mNotificationManagerService.addNotification(r); } // another package Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) .build(); StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "tag", uid, 0, n, new UserHandle(uid), null, 0); NotificationRecord otherPackage = new NotificationRecord(mContext, sbn, mTestNotificationChannel); mNotificationManagerService.addEnqueuedNotification(otherPackage); mNotificationManagerService.addNotification(otherPackage); // Same notifications are enqueued as posted, everything counts b/c id and tag don't match assertEquals(40, mNotificationManagerService.getNotificationCountLocked(PKG, new UserHandle(uid).getIdentifier(), 0, null)); assertEquals(40, mNotificationManagerService.getNotificationCountLocked(PKG, new UserHandle(uid).getIdentifier(), 0, "tag2")); assertEquals(2, mNotificationManagerService.getNotificationCountLocked("a", new UserHandle(uid).getIdentifier(), 0, "banana")); // exclude a known notification - it's excluded from only the posted list, not enqueued assertEquals(39, mNotificationManagerService.getNotificationCountLocked(PKG, new UserHandle(uid).getIdentifier(), 0, "tag")); } } tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java +631 −632 Original line number Diff line number Diff line Loading @@ -1212,4 +1212,3 @@ public class NotificationTestList extends TestActivity return Bitmap.createBitmap(bd.getBitmap()); } } Loading
core/java/android/app/Notification.java +16 −0 Original line number Diff line number Diff line Loading @@ -2543,6 +2543,22 @@ public class Notification implements Parcelable } } /** * @hide */ public boolean hasCompletedProgress() { // not a progress notification; can't be complete if (!extras.containsKey(EXTRA_PROGRESS) || !extras.containsKey(EXTRA_PROGRESS_MAX)) { return false; } // many apps use max 0 for 'indeterminate'; not complete if (extras.getInt(EXTRA_PROGRESS_MAX) == 0) { return false; } return extras.getInt(EXTRA_PROGRESS) == extras.getInt(EXTRA_PROGRESS_MAX); } /** @removed */ @Deprecated public String getChannel() { Loading
core/tests/coretests/src/android/app/NotificationTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -99,6 +99,45 @@ public class NotificationTest { assertTrue(satisfiesTextContrast(secondaryTextColor, backgroundColor)); } @Test public void testHasCompletedProgress_noProgress() { Notification n = new Notification.Builder(mContext).build(); assertFalse(n.hasCompletedProgress()); } @Test public void testHasCompletedProgress_complete() { Notification n = new Notification.Builder(mContext) .setProgress(100, 100, true) .build(); Notification n2 = new Notification.Builder(mContext) .setProgress(10, 10, false) .build(); assertTrue(n.hasCompletedProgress()); assertTrue(n2.hasCompletedProgress()); } @Test public void testHasCompletedProgress_notComplete() { Notification n = new Notification.Builder(mContext) .setProgress(100, 99, true) .build(); Notification n2 = new Notification.Builder(mContext) .setProgress(10, 4, false) .build(); assertFalse(n.hasCompletedProgress()); assertFalse(n2.hasCompletedProgress()); } @Test public void testHasCompletedProgress_zeroMax() { Notification n = new Notification.Builder(mContext) .setProgress(0, 0, true) .build(); assertFalse(n.hasCompletedProgress()); } private Notification.Builder getMediaNotification() { MediaSession session = new MediaSession(mContext, "test"); return new Notification.Builder(mContext, "color") Loading
services/core/java/com/android/server/notification/NotificationManagerService.java +47 −28 Original line number Diff line number Diff line Loading @@ -211,7 +211,7 @@ public class NotificationManagerService extends SystemService { = SystemProperties.getBoolean("debug.child_notifs", true); static final int MAX_PACKAGE_NOTIFICATIONS = 50; static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 10f; static final float DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE = 5f; // message codes static final int MESSAGE_TIMEOUT = 2; Loading Loading @@ -3472,8 +3472,19 @@ public class NotificationManagerService extends SystemService { // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { synchronized (mNotificationLock) { if (mNotificationsByKey.get(r.sbn.getKey()) != null) { // this is an update, rate limit updates only if (mNotificationsByKey.get(r.sbn.getKey()) == null && isCallerInstantApp(pkg)) { // Ephemeral apps have some special constraints for notifications. // They are not allowed to create new notifications however they are allowed to // update notifications created by the system (e.g. a foreground service // notification). throw new SecurityException("Instant app " + pkg + " cannot create notifications"); } // rate limit updates that aren't completed progress notifications if (mNotificationsByKey.get(r.sbn.getKey()) != null && !r.getNotification().hasCompletedProgress()) { final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg); if (appEnqueueRate > mMaxPackageEnqueueRate) { mUsageStats.registerOverRateQuota(pkg); Loading @@ -3485,36 +3496,18 @@ public class NotificationManagerService extends SystemService { } return false; } } else if (isCallerInstantApp(pkg)) { // Ephemeral apps have some special constraints for notifications. // They are not allowed to create new notifications however they are allowed to // update notifications created by the system (e.g. a foreground service // notification). throw new SecurityException("Instant app " + pkg + " cannot create notifications"); } int count = 0; final int N = mNotificationList.size(); for (int i=0; i<N; i++) { final NotificationRecord existing = mNotificationList.get(i); if (existing.sbn.getPackageName().equals(pkg) && existing.sbn.getUserId() == userId) { if (existing.sbn.getId() == id && TextUtils.equals(existing.sbn.getTag(), tag)) { break; // Allow updating existing notification } count++; // limit the number of outstanding notificationrecords an app can have int count = getNotificationCountLocked(pkg, userId, id, tag); if (count >= MAX_PACKAGE_NOTIFICATIONS) { mUsageStats.registerOverCountQuota(pkg); Slog.e(TAG, "Package has already posted " + count Slog.e(TAG, "Package has already posted or enqueued " + count + " notifications. Not showing more. package=" + pkg); return false; } } } } } // snoozed apps if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) { Loading @@ -3538,6 +3531,32 @@ public class NotificationManagerService extends SystemService { return true; } protected int getNotificationCountLocked(String pkg, int userId, int excludedId, String excludedTag) { int count = 0; final int N = mNotificationList.size(); for (int i = 0; i < N; i++) { final NotificationRecord existing = mNotificationList.get(i); if (existing.sbn.getPackageName().equals(pkg) && existing.sbn.getUserId() == userId) { if (existing.sbn.getId() == excludedId && TextUtils.equals(existing.sbn.getTag(), excludedTag)) { continue; } count++; } } final int M = mEnqueuedNotifications.size(); for (int i = 0; i < M; i++) { final NotificationRecord existing = mEnqueuedNotifications.get(i); if (existing.sbn.getPackageName().equals(pkg) && existing.sbn.getUserId() == userId) { count++; } } return count; } protected boolean isBlocked(NotificationRecord r, NotificationUsageStats usageStats) { final String pkg = r.sbn.getPackageName(); final int callingUid = r.sbn.getUid(); Loading
services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java +33 −0 Original line number Diff line number Diff line Loading @@ -1242,4 +1242,37 @@ public class NotificationManagerServiceTest extends NotificationTestCase { assertFalse(posted.getNotification().isColorized()); } @Test public void testGetNotificationCountLocked() throws Exception { for (int i = 0; i < 20; i++) { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, i, null, false); mNotificationManagerService.addEnqueuedNotification(r); } for (int i = 0; i < 20; i++) { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, i, null, false); mNotificationManagerService.addNotification(r); } // another package Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) .build(); StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "tag", uid, 0, n, new UserHandle(uid), null, 0); NotificationRecord otherPackage = new NotificationRecord(mContext, sbn, mTestNotificationChannel); mNotificationManagerService.addEnqueuedNotification(otherPackage); mNotificationManagerService.addNotification(otherPackage); // Same notifications are enqueued as posted, everything counts b/c id and tag don't match assertEquals(40, mNotificationManagerService.getNotificationCountLocked(PKG, new UserHandle(uid).getIdentifier(), 0, null)); assertEquals(40, mNotificationManagerService.getNotificationCountLocked(PKG, new UserHandle(uid).getIdentifier(), 0, "tag2")); assertEquals(2, mNotificationManagerService.getNotificationCountLocked("a", new UserHandle(uid).getIdentifier(), 0, "banana")); // exclude a known notification - it's excluded from only the posted list, not enqueued assertEquals(39, mNotificationManagerService.getNotificationCountLocked(PKG, new UserHandle(uid).getIdentifier(), 0, "tag")); } }
tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java +631 −632 Original line number Diff line number Diff line Loading @@ -1212,4 +1212,3 @@ public class NotificationTestList extends TestActivity return Bitmap.createBitmap(bd.getBitmap()); } }