Merge changes from topic "154198299"
* changes: Implement rate limiting toasts. Implement multi rate limiter.
This commit is contained in:
committed by
Android (Google) Code Review
commit
882d3e8825
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user