diff options
| author | 2020-11-30 13:10:12 +0000 | |
|---|---|---|
| committer | 2020-12-03 17:03:11 +0000 | |
| commit | 09027467cd7319fa1c86f23f844f568d6952733d (patch) | |
| tree | 21182005632f791d3d19a35b232a7d38b16fb232 | |
| parent | 66b8a14aa215b3d0351417f49ab92cf71aeeffb6 (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
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; |