summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jan Tomljanovic <jtomljanovic@google.com> 2020-11-30 13:10:12 +0000
committer Jan Tomljanovic <jtomljanovic@google.com> 2020-12-03 17:03:11 +0000
commit09027467cd7319fa1c86f23f844f568d6952733d (patch)
tree21182005632f791d3d19a35b232a7d38b16fb232
parent66b8a14aa215b3d0351417f49ab92cf71aeeffb6 (diff)
Implement rate limiting toasts.
We rate limit showing toasts on a per package basis. Each time the app hits the limit, any further toast attempted to be shown will be discarded. Specific rate limits are designed in a way such that if the app continuously posts toasts, the period for which it will be blocked from posting gradually increases each time it hits the limit. Test: atest android.widget.cts.ToastTest Test: atest NotificationManagerServiceTest Bug: 154198299 Change-Id: I41656745cbd4e6cb6650cf4100ca32a09dc67810
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java54
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java69
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java4
3 files changed, 121 insertions, 6 deletions
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index dfeb6822c8e8..cfc1e29ba341 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -276,6 +276,7 @@ import com.android.server.pm.PackageManagerService;
import com.android.server.policy.PhoneWindowManager;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.BackgroundActivityStartCallback;
import com.android.server.wm.WindowManagerInternal;
@@ -299,6 +300,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -372,6 +374,20 @@ public class NotificationManagerService extends SystemService {
RoleManager.ROLE_EMERGENCY
};
+ // Used for rate limiting toasts by package.
+ static final String TOAST_QUOTA_TAG = "toast_quota_tag";
+
+ // This constant defines rate limits applied to showing toasts. The numbers are set in a way
+ // such that an aggressive toast showing strategy would result in a roughly 1.5x longer wait
+ // time (before the package is allowed to show toasts again) each time the toast rate limit is
+ // reached. It's meant to protect the user against apps spamming them with toasts (either
+ // accidentally or on purpose).
+ private static final MultiRateLimiter.RateLimit[] TOAST_RATE_LIMITS = {
+ MultiRateLimiter.RateLimit.create(3, Duration.ofSeconds(20)),
+ MultiRateLimiter.RateLimit.create(5, Duration.ofSeconds(42)),
+ MultiRateLimiter.RateLimit.create(6, Duration.ofSeconds(68)),
+ };
+
// When #matchesCallFilter is called from the ringer, wait at most
// 3s to resolve the contacts. This timeout is required since
// ContactsProvider might take a long time to start up.
@@ -423,6 +439,16 @@ public class NotificationManagerService extends SystemService {
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
private static final long NOTIFICATION_TRAMPOLINE_BLOCK = 167676448L;
+ /**
+ * Rate limit showing toasts, on a per package basis.
+ *
+ * It limits the effects of {@link android.widget.Toast#show()} calls to prevent overburdening
+ * the user with too many toasts in a limited time. Any attempt to show more toasts than allowed
+ * in a certain time frame will result in the toast being discarded.
+ */
+ @ChangeId
+ private static final long RATE_LIMIT_TOASTS = 154198299L;
+
private IActivityManager mAm;
private ActivityTaskManagerInternal mAtm;
private ActivityManager mActivityManager;
@@ -501,6 +527,9 @@ public class NotificationManagerService extends SystemService {
@GuardedBy("mToastQueue")
private boolean mIsCurrentToastShown = false;
+ // Used for rate limiting toasts by package.
+ private MultiRateLimiter mToastRateLimiter;
+
// The last key in this list owns the hardware.
ArrayList<String> mLights = new ArrayList<>();
@@ -1907,7 +1936,8 @@ public class NotificationManagerService extends SystemService {
DevicePolicyManagerInternal dpm, IUriGrantsManager ugm,
UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager,
NotificationHistoryManager historyManager, StatsManager statsManager,
- TelephonyManager telephonyManager, ActivityManagerInternal ami) {
+ TelephonyManager telephonyManager, ActivityManagerInternal ami,
+ MultiRateLimiter toastRateLimiter) {
mHandler = handler;
Resources resources = getContext().getResources();
mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
@@ -2099,6 +2129,8 @@ public class NotificationManagerService extends SystemService {
com.android.internal.R.array.config_notificationMsgPkgsAllowedAsConvos));
mStatsManager = statsManager;
+ mToastRateLimiter = toastRateLimiter;
+
// register for various Intents.
// If this is called within a test, make sure to unregister the intent receivers by
// calling onDestroy()
@@ -2209,7 +2241,8 @@ public class NotificationManagerService extends SystemService {
mStatsManager = (StatsManager) getContext().getSystemService(
Context.STATS_MANAGER),
getContext().getSystemService(TelephonyManager.class),
- LocalServices.getService(ActivityManagerInternal.class));
+ LocalServices.getService(ActivityManagerInternal.class),
+ createToastRateLimiter());
publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -2847,6 +2880,10 @@ public class NotificationManagerService extends SystemService {
return mInternalService;
}
+ private MultiRateLimiter createToastRateLimiter() {
+ return new MultiRateLimiter.Builder(getContext()).addRateLimits(TOAST_RATE_LIMITS).build();
+ }
+
@VisibleForTesting
final IBinder mService = new INotificationManager.Stub() {
// Toasts
@@ -7310,10 +7347,21 @@ public class NotificationManagerService extends SystemService {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
- if (record.show()) {
+ int userId = UserHandle.getUserId(record.uid);
+ boolean rateLimitingEnabled =
+ CompatChanges.isChangeEnabled(RATE_LIMIT_TOASTS, record.uid);
+ boolean isWithinQuota =
+ mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG);
+ if ((!rateLimitingEnabled || isWithinQuota) && record.show()) {
scheduleDurationReachedLocked(record);
mIsCurrentToastShown = true;
+ if (rateLimitingEnabled) {
+ mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
+ }
return;
+ } else if (rateLimitingEnabled && !isWithinQuota) {
+ Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
+ + "following toast was blocked and discarded: " + record);
}
int index = mToastQueue.indexOf(record);
if (index >= 0) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 09a4289ece3f..d624868423db 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -182,6 +182,7 @@ import com.android.server.notification.NotificationManagerService.NotificationAs
import com.android.server.notification.NotificationManagerService.NotificationListeners;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;
@@ -296,6 +297,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
NotificationHistoryManager mHistoryManager;
@Mock
StatsManager mStatsManager;
+ @Mock
+ MultiRateLimiter mToastRateLimiter;
BroadcastReceiver mPackageIntentReceiver;
NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
@@ -485,7 +488,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mGroupHelper, mAm, mAtm, mAppUsageStats,
mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal,
mAppOpsManager, mUm, mHistoryManager, mStatsManager,
- mock(TelephonyManager.class), mAmi);
+ mock(TelephonyManager.class), mAmi, mToastRateLimiter);
mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
mService.setAudioManager(mAudioManager);
@@ -565,7 +568,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
try {
mService.onDestroy();
- } catch (IllegalStateException e) {
+ } catch (IllegalStateException | IllegalArgumentException e) {
// can throw if a broadcast receiver was never registered
}
@@ -4887,6 +4890,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -4909,6 +4913,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -4927,6 +4932,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -4949,10 +4955,32 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testToastRateLimiterCanPreventsShowCallForCustomToast() throws Exception {
+ final String testPackage = "testPackageName";
+ assertEquals(0, mService.mToastQueue.size());
+ mService.isSystemUid = false;
+ setToastRateIsWithinQuota(false); // rate limit reached
+
+ // package is not suspended
+ when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
+ .thenReturn(false);
+
+ setAppInForegroundForToasts(mUid, true);
+
+ Binder token = new Binder();
+ ITransientNotification callback = mock(ITransientNotification.class);
+ INotificationManager nmService = (INotificationManager) mService.mService;
+
+ nmService.enqueueToast(testPackage, token, callback, 2000, 0);
+ verify(callback, times(0)).show(any());
+ }
+
+ @Test
public void testAllowForegroundTextToasts() throws Exception {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -4971,6 +4999,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -4989,6 +5018,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5012,11 +5042,31 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testToastRateLimiterCanPreventsShowCallForTextToast() throws Exception {
+ final String testPackage = "testPackageName";
+ assertEquals(0, mService.mToastQueue.size());
+ mService.isSystemUid = false;
+ setToastRateIsWithinQuota(false); // rate limit reached
+
+ // package is not suspended
+ when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
+ .thenReturn(false);
+
+ Binder token = new Binder();
+ INotificationManager nmService = (INotificationManager) mService.mService;
+
+ nmService.enqueueTextToast(testPackage, token, "Text", 2000, 0, null);
+ verify(mStatusBar, times(0))
+ .showToast(anyInt(), any(), any(), any(), any(), anyInt(), any());
+ }
+
+ @Test
public void backgroundSystemCustomToast_callsSetProcessImportantAsForegroundForToast() throws
Exception {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = true;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5041,6 +5091,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5061,6 +5112,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5080,6 +5132,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5096,6 +5149,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5116,6 +5170,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5138,6 +5193,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = true;
+ setToastRateIsWithinQuota(true);
// package is suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5160,6 +5216,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final String testPackage = "testPackageName";
assertEquals(0, mService.mToastQueue.size());
mService.isSystemUid = false;
+ setToastRateIsWithinQuota(true);
// package is not suspended
when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid)))
@@ -5187,6 +5244,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
when(mAtm.hasResumedActivity(uid)).thenReturn(inForeground);
}
+ private void setToastRateIsWithinQuota(boolean isWithinQuota) {
+ when(mToastRateLimiter.isWithinQuota(
+ anyInt(),
+ anyString(),
+ eq(NotificationManagerService.TOAST_QUOTA_TAG)))
+ .thenReturn(isWithinQuota);
+ }
+
@Test
public void testOnPanelRevealedAndHidden() {
int items = 5;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
index a80f62ab09ee..4ce237e3aadc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
@@ -67,6 +67,7 @@ import com.android.server.lights.LightsManager;
import com.android.server.notification.NotificationManagerService.NotificationAssistants;
import com.android.server.notification.NotificationManagerService.NotificationListeners;
import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;
@@ -156,7 +157,8 @@ public class RoleObserverTest extends UiServiceTestCase {
mock(UriGrantsManagerInternal.class),
mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class),
mock(StatsManager.class), mock(TelephonyManager.class),
- mock(ActivityManagerInternal.class));
+ mock(ActivityManagerInternal.class),
+ mock(MultiRateLimiter.class));
} catch (SecurityException e) {
if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
throw e;