Merge changes from topic "154198299"

* changes:
  Implement rate limiting toasts.
  Implement multi rate limiter.
This commit is contained in:
Jan Tomljanovic
2020-12-04 14:00:07 +00:00
committed by Android (Google) Code Review
5 changed files with 501 additions and 6 deletions

View File

@@ -277,6 +277,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;
@@ -298,6 +299,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;
@@ -371,6 +373,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.
@@ -422,6 +438,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;
@@ -500,6 +526,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<>();
@@ -1915,7 +1944,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(),
@@ -2107,6 +2137,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()
@@ -2217,7 +2249,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);
@@ -2855,6 +2888,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
@@ -7329,10 +7366,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) {

View File

@@ -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:
*
* <li>5 times in 20 seconds</li>
* and
* <li>6 times in 40 seconds</li>
* and
* <li>10 times in 1 hour</li>
*
* <p><br>
* 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}.
* </p>
*
* @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<CountQuotaTracker> 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<CountQuotaTracker> 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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -183,6 +183,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;
@@ -295,6 +296,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(
@@ -486,7 +489,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);
@@ -566,7 +569,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
}
@@ -4888,6 +4891,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)))
@@ -4910,6 +4914,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)))
@@ -4928,6 +4933,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,11 +4955,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)))
@@ -4972,6 +5000,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)))
@@ -4990,6 +5019,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,12 +5042,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)))
@@ -5042,6 +5092,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)))
@@ -5062,6 +5113,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)))
@@ -5081,6 +5133,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)))
@@ -5097,6 +5150,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)))
@@ -5117,6 +5171,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)))
@@ -5139,6 +5194,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)))
@@ -5161,6 +5217,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)))
@@ -5188,6 +5245,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;

View File

@@ -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;