From 66b8a14aa215b3d0351417f49ab92cf71aeeffb6 Mon Sep 17 00:00:00 2001 From: Jan Tomljanovic Date: Mon, 30 Nov 2020 13:06:37 +0000 Subject: [PATCH 1/2] Implement multi rate limiter. Implement a class that can keep track of multiple quotas at the same time. Test: atest MultiRateLimiterTest Bug: 154198299 Change-Id: If2428f0a2c21cf91d250fec4ecf182550e00977f --- .../server/utils/quota/MultiRateLimiter.java | 183 ++++++++++++++++ .../utils/quota/MultiRateLimiterTest.java | 197 ++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 services/core/java/com/android/server/utils/quota/MultiRateLimiter.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/utils/quota/MultiRateLimiterTest.java diff --git a/services/core/java/com/android/server/utils/quota/MultiRateLimiter.java b/services/core/java/com/android/server/utils/quota/MultiRateLimiter.java new file mode 100644 index 0000000000000..fdbe4b425d39a --- /dev/null +++ b/services/core/java/com/android/server/utils/quota/MultiRateLimiter.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.utils.quota; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * Can be used to rate limit events per app based on multiple rates at the same time. For example, + * it can limit an event to happen only: + * + *
  • 5 times in 20 seconds
  • + * and + *
  • 6 times in 40 seconds
  • + * and + *
  • 10 times in 1 hour
  • + * + *


    + * All listed rates apply at the same time, and the UPTC will be out of quota if it doesn't satisfy + * all the given rates. The underlying mechanism used is + * {@link com.android.server.utils.quota.CountQuotaTracker}, so all its conditions apply, as well + * as an additional constraint: all the user-package-tag combinations (UPTC) are considered to be in + * the same {@link com.android.server.utils.quota.Category}. + *

    + * + * @hide + */ +public class MultiRateLimiter { + + private static final CountQuotaTracker[] EMPTY_TRACKER_ARRAY = {}; + + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final CountQuotaTracker[] mQuotaTrackers; + + private MultiRateLimiter(List quotaTrackers) { + mQuotaTrackers = quotaTrackers.toArray(EMPTY_TRACKER_ARRAY); + } + + /** Record that an event happened and count it towards the given quota. */ + public void noteEvent(int userId, @NonNull String packageName, @Nullable String tag) { + synchronized (mLock) { + noteEventLocked(userId, packageName, tag); + } + } + + /** Check whether the given UPTC is allowed to trigger an event. */ + public boolean isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag) { + synchronized (mLock) { + return isWithinQuotaLocked(userId, packageName, tag); + } + } + + @GuardedBy("mLock") + private void noteEventLocked(int userId, @NonNull String packageName, @Nullable String tag) { + for (CountQuotaTracker quotaTracker : mQuotaTrackers) { + quotaTracker.noteEvent(userId, packageName, tag); + } + } + + @GuardedBy("mLock") + private boolean isWithinQuotaLocked(int userId, @NonNull String packageName, + @Nullable String tag) { + for (CountQuotaTracker quotaTracker : mQuotaTrackers) { + if (!quotaTracker.isWithinQuota(userId, packageName, tag)) { + return false; + } + } + return true; + } + + /** Can create a new {@link MultiRateLimiter}. */ + public static class Builder { + + private final List mQuotaTrackers; + private final Context mContext; + private final Categorizer mCategorizer; + private final Category mCategory; + @Nullable private final QuotaTracker.Injector mInjector; + + /** + * Creates a new builder and allows to inject an object that can be used + * to manipulate elapsed time in tests. + */ + @VisibleForTesting + Builder(Context context, QuotaTracker.Injector injector) { + this.mQuotaTrackers = new ArrayList<>(); + this.mContext = context; + this.mInjector = injector; + this.mCategorizer = Categorizer.SINGLE_CATEGORIZER; + this.mCategory = Category.SINGLE_CATEGORY; + } + + /** Creates a new builder for {@link MultiRateLimiter}. */ + public Builder(Context context) { + this(context, null); + } + + /** + * Adds another rate limit to be used in {@link MultiRateLimiter}. + * + * @param limit The maximum event count an app can have in the rolling time window. + * @param windowSize The rolling time window to use when checking quota usage. + */ + public Builder addRateLimit(int limit, Duration windowSize) { + CountQuotaTracker countQuotaTracker; + if (mInjector != null) { + countQuotaTracker = new CountQuotaTracker(mContext, mCategorizer, mInjector); + } else { + countQuotaTracker = new CountQuotaTracker(mContext, mCategorizer); + } + countQuotaTracker.setCountLimit(mCategory, limit, windowSize.toMillis()); + mQuotaTrackers.add(countQuotaTracker); + return this; + } + + /** Adds another rate limit to be used in {@link MultiRateLimiter}. */ + public Builder addRateLimit(@NonNull RateLimit rateLimit) { + return addRateLimit(rateLimit.mLimit, rateLimit.mWindowSize); + } + + /** Adds all given rate limits that will be used in {@link MultiRateLimiter}. */ + public Builder addRateLimits(@NonNull RateLimit[] rateLimits) { + for (RateLimit rateLimit : rateLimits) { + addRateLimit(rateLimit); + } + return this; + } + + /** + * Return a new {@link com.android.server.utils.quota.MultiRateLimiter} using set rate + * limit. + */ + public MultiRateLimiter build() { + return new MultiRateLimiter(mQuotaTrackers); + } + } + + /** Helper class that describes a rate limit. */ + public static class RateLimit { + public final int mLimit; + public final Duration mWindowSize; + + /** + * @param limit The maximum count of some occurrence in the rolling time window. + * @param windowSize The rolling time window to use when checking quota usage. + */ + private RateLimit(int limit, Duration windowSize) { + this.mLimit = limit; + this.mWindowSize = windowSize; + } + + /** + * @param limit The maximum count of some occurrence in the rolling time window. + * @param windowSize The rolling time window to use when checking quota usage. + */ + public static RateLimit create(int limit, Duration windowSize) { + return new RateLimit(limit, windowSize); + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/MultiRateLimiterTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/MultiRateLimiterTest.java new file mode 100644 index 0000000000000..df533f3c122a6 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/MultiRateLimiterTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.utils.quota; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; + +import java.time.Duration; + +@SmallTest +public class MultiRateLimiterTest { + + private static final int USER_ID = 1; + private static final String PACKAGE_NAME_1 = "com.android.package.one"; + private static final String PACKAGE_NAME_2 = "com.android.package.two"; + private static final String TAG = "tag"; + + @Rule + public final TestableContext mContext = + new TestableContext(InstrumentationRegistry.getContext(), null); + + private final InjectorForTest mInjector = new InjectorForTest(); + + private static class InjectorForTest extends QuotaTracker.Injector { + Duration mElapsedTime = Duration.ZERO; + + @Override + public long getElapsedRealtime() { + return mElapsedTime.toMillis(); + } + + @Override + public boolean isAlarmManagerReady() { + return true; + } + } + + @Test + public void testSingleRateLimit_belowLimit_isWithinQuota() { + MultiRateLimiter multiRateLimiter = new MultiRateLimiter.Builder(mContext, mInjector) + .addRateLimit(3, Duration.ofSeconds(20)) + .build(); + + // Three quick events are within quota. + mInjector.mElapsedTime = Duration.ZERO; + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(50); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(100); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + } + + @Test + public void testSingleRateLimit_aboveLimit_isNotWithinQuota() { + MultiRateLimiter multiRateLimiter = new MultiRateLimiter.Builder(mContext, mInjector) + .addRateLimit(3, Duration.ofSeconds(20)) + .build(); + + mInjector.mElapsedTime = Duration.ZERO; + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(50); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(100); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(150); + // We hit the limit, 4th event in under 20 seconds is not within quota. + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse(); + } + + @Test + public void testSingleRateLimit_afterGoingAboveQuotaAndWaitingWindow_isBackWithinQuota() { + MultiRateLimiter multiRateLimiter = new MultiRateLimiter.Builder(mContext, mInjector) + .addRateLimit(3, Duration.ofSeconds(20)) + .build(); + + mInjector.mElapsedTime = Duration.ZERO; + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(50); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(100); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(150); + // We hit the limit, 4th event in under 20 seconds is not within quota. + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse(); + + mInjector.mElapsedTime = Duration.ofSeconds(21); + // 20 seconds have passed, we're again within quota. + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + } + + @Test + public void createMultipleRateLimits_testTheyLimitsAsExpected() { + MultiRateLimiter multiRateLimiter = new MultiRateLimiter.Builder(mContext, mInjector) + .addRateLimit(3, Duration.ofSeconds(20)) // 1st limit + .addRateLimit(4, Duration.ofSeconds(40)) // 2nd limit + .addRateLimit(5, Duration.ofSeconds(60)) // 3rd limit + .build(); + + // Testing the 1st limit + mInjector.mElapsedTime = Duration.ZERO; + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(50); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(100); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(150); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse(); + + mInjector.mElapsedTime = Duration.ofSeconds(21); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + // Testing the 2nd limit + mInjector.mElapsedTime = Duration.ofSeconds(35); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse(); + + mInjector.mElapsedTime = Duration.ofSeconds(42); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + // Testing the 3rd limit. + mInjector.mElapsedTime = Duration.ofSeconds(43); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse(); + + mInjector.mElapsedTime = Duration.ofSeconds(62); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + } + + @Test + public void createSingleRateLimit_testItLimitsOnlyGivenUptc() { + MultiRateLimiter multiRateLimiter = new MultiRateLimiter.Builder(mContext, mInjector) + .addRateLimit(3, Duration.ofSeconds(20)) + .build(); + + mInjector.mElapsedTime = Duration.ZERO; + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(50); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(100); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue(); + multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG); + + mInjector.mElapsedTime = Duration.ofMillis(150); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse(); + // Different userId - packageName - tag combination is still allowed. + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue(); + + mInjector.mElapsedTime = Duration.ofSeconds(21); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue(); + assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue(); + } +} From 09027467cd7319fa1c86f23f844f568d6952733d Mon Sep 17 00:00:00 2001 From: Jan Tomljanovic Date: Mon, 30 Nov 2020 13:10:12 +0000 Subject: [PATCH 2/2] 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 --- .../NotificationManagerService.java | 54 ++++++++++++++- .../NotificationManagerServiceTest.java | 69 ++++++++++++++++++- .../server/notification/RoleObserverTest.java | 4 +- 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 dfeb6822c8e8f..cfc1e29ba3418 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 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 09a4289ece3f3..d624868423db2 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))) @@ -4948,11 +4954,33 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(callback, times(1)).show(any()); } + @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))) @@ -5011,12 +5041,32 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .showToast(anyInt(), any(), any(), any(), any(), anyInt(), any()); } + @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 a80f62ab09eef..4ce237e3aadcd 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;