blob: a8daa3ba07a26297854b1b17bcee500c9251fdf3 [file] [log] [blame]
/*
* 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 String TAG = "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);
}
}
/** Remove all saved events from the rate limiter for the given app (reset it). */
public void clear(int userId, @NonNull String packageName) {
synchronized (mLock) {
clearLocked(userId, packageName);
}
}
@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;
}
@GuardedBy("mLock")
private void clearLocked(int userId, @NonNull String packageName) {
for (CountQuotaTracker quotaTracker : mQuotaTrackers) {
// This method behaves as if the package has been removed from the device, which
// isn't the case here, but it does similar clean-up to what we are aiming for here,
// so it works for this use case.
quotaTracker.onAppRemovedLocked(userId, packageName);
}
}
/** 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);
}
}
}