Limit the number of notis an app can enqueue

In addition to the number they can post. Also decrease
the number of posts per second to 5, but allow finished
progress notifications through.

Fixes: 63173849
Fixes: 63167456
Test: runtests systemui-notification & simultaneously
downloading 5 files.
Change-Id: I0c8ea35b3d9d38b000ea5fe383515e1d5a26bac7
This commit is contained in:
Julia Reynolds
2017-07-05 08:47:03 -04:00
parent 5f8e0b881e
commit 6ad0aecf99
5 changed files with 768 additions and 662 deletions

View File

@@ -2541,6 +2541,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() {

View File

@@ -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")

View File

@@ -209,7 +209,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;
@@ -3292,8 +3292,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);
@@ -3305,33 +3316,15 @@ 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++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
mUsageStats.registerOverCountQuota(pkg);
Slog.e(TAG, "Package has already posted " + count
+ " notifications. Not showing more. package=" + pkg);
return false;
}
}
// 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 or enqueued " + count
+ " notifications. Not showing more. package=" + pkg);
return false;
}
}
}
@@ -3358,6 +3351,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();

View File

@@ -984,4 +984,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"));
}
}