1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.utils.quota;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 
23 import com.android.internal.annotations.GuardedBy;
24 import com.android.internal.annotations.VisibleForTesting;
25 
26 import java.time.Duration;
27 import java.util.ArrayList;
28 import java.util.List;
29 
30 /**
31  * Can be used to rate limit events per app based on multiple rates at the same time. For example,
32  * it can limit an event to happen only:
33  *
34  * <li>5 times in 20 seconds</li>
35  * and
36  * <li>6 times in 40 seconds</li>
37  * and
38  * <li>10 times in 1 hour</li>
39  *
40  * <p><br>
41  * All listed rates apply at the same time, and the UPTC will be out of quota if it doesn't satisfy
42  * all the given rates. The underlying mechanism used is
43  * {@link com.android.server.utils.quota.CountQuotaTracker}, so all its conditions apply, as well
44  * as an additional constraint: all the user-package-tag combinations (UPTC) are considered to be in
45  * the same {@link com.android.server.utils.quota.Category}.
46  * </p>
47  *
48  * @hide
49  */
50 public class MultiRateLimiter {
51     private static final String TAG = "MultiRateLimiter";
52 
53     private static final CountQuotaTracker[] EMPTY_TRACKER_ARRAY = {};
54 
55     private final Object mLock = new Object();
56     @GuardedBy("mLock")
57     private final CountQuotaTracker[] mQuotaTrackers;
58 
MultiRateLimiter(List<CountQuotaTracker> quotaTrackers)59     private MultiRateLimiter(List<CountQuotaTracker> quotaTrackers) {
60         mQuotaTrackers = quotaTrackers.toArray(EMPTY_TRACKER_ARRAY);
61     }
62 
63     /** Record that an event happened and count it towards the given quota. */
noteEvent(int userId, @NonNull String packageName, @Nullable String tag)64     public void noteEvent(int userId, @NonNull String packageName, @Nullable String tag) {
65         synchronized (mLock) {
66             noteEventLocked(userId, packageName, tag);
67         }
68     }
69 
70     /** Check whether the given UPTC is allowed to trigger an event. */
isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag)71     public boolean isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag) {
72         synchronized (mLock) {
73             return isWithinQuotaLocked(userId, packageName, tag);
74         }
75     }
76 
77     /** Remove all saved events from the rate limiter for the given app (reset it). */
clear(int userId, @NonNull String packageName)78     public void clear(int userId, @NonNull String packageName) {
79         synchronized (mLock) {
80             clearLocked(userId, packageName);
81         }
82     }
83 
84     @GuardedBy("mLock")
noteEventLocked(int userId, @NonNull String packageName, @Nullable String tag)85     private void noteEventLocked(int userId, @NonNull String packageName, @Nullable String tag) {
86         for (CountQuotaTracker quotaTracker : mQuotaTrackers) {
87             quotaTracker.noteEvent(userId, packageName, tag);
88         }
89     }
90 
91     @GuardedBy("mLock")
isWithinQuotaLocked(int userId, @NonNull String packageName, @Nullable String tag)92     private boolean isWithinQuotaLocked(int userId, @NonNull String packageName,
93             @Nullable String tag) {
94         for (CountQuotaTracker quotaTracker : mQuotaTrackers) {
95             if (!quotaTracker.isWithinQuota(userId, packageName, tag)) {
96                 return false;
97             }
98         }
99         return true;
100     }
101 
102     @GuardedBy("mLock")
clearLocked(int userId, @NonNull String packageName)103     private void clearLocked(int userId, @NonNull String packageName) {
104         for (CountQuotaTracker quotaTracker : mQuotaTrackers) {
105             // This method behaves as if the package has been removed from the device, which
106             // isn't the case here, but it does similar clean-up to what we are aiming for here,
107             // so it works for this use case.
108             quotaTracker.onAppRemovedLocked(userId, packageName);
109         }
110     }
111 
112     /** Can create a new {@link MultiRateLimiter}. */
113     public static class Builder {
114 
115         private final List<CountQuotaTracker> mQuotaTrackers;
116         private final Context mContext;
117         private final Categorizer mCategorizer;
118         private final Category mCategory;
119         @Nullable
120         private final QuotaTracker.Injector mInjector;
121 
122         /**
123          * Creates a new builder and allows to inject an object that can be used
124          * to manipulate elapsed time in tests.
125          */
126         @VisibleForTesting
Builder(Context context, QuotaTracker.Injector injector)127         Builder(Context context, QuotaTracker.Injector injector) {
128             this.mQuotaTrackers = new ArrayList<>();
129             this.mContext = context;
130             this.mInjector = injector;
131             this.mCategorizer = Categorizer.SINGLE_CATEGORIZER;
132             this.mCategory = Category.SINGLE_CATEGORY;
133         }
134 
135         /** Creates a new builder for {@link MultiRateLimiter}. */
Builder(Context context)136         public Builder(Context context) {
137             this(context, null);
138         }
139 
140         /**
141          * Adds another rate limit to be used in {@link MultiRateLimiter}.
142          *
143          * @param limit      The maximum event count an app can have in the rolling time window.
144          * @param windowSize The rolling time window to use when checking quota usage.
145          */
addRateLimit(int limit, Duration windowSize)146         public Builder addRateLimit(int limit, Duration windowSize) {
147             CountQuotaTracker countQuotaTracker;
148             if (mInjector != null) {
149                 countQuotaTracker = new CountQuotaTracker(mContext, mCategorizer, mInjector);
150             } else {
151                 countQuotaTracker = new CountQuotaTracker(mContext, mCategorizer);
152             }
153             countQuotaTracker.setCountLimit(mCategory, limit, windowSize.toMillis());
154             mQuotaTrackers.add(countQuotaTracker);
155             return this;
156         }
157 
158         /** Adds another rate limit to be used in {@link MultiRateLimiter}. */
addRateLimit(@onNull RateLimit rateLimit)159         public Builder addRateLimit(@NonNull RateLimit rateLimit) {
160             return addRateLimit(rateLimit.mLimit, rateLimit.mWindowSize);
161         }
162 
163         /** Adds all given rate limits that will be used in {@link MultiRateLimiter}. */
addRateLimits(@onNull RateLimit[] rateLimits)164         public Builder addRateLimits(@NonNull RateLimit[] rateLimits) {
165             for (RateLimit rateLimit : rateLimits) {
166                 addRateLimit(rateLimit);
167             }
168             return this;
169         }
170 
171         /**
172          * Return a new {@link com.android.server.utils.quota.MultiRateLimiter} using set rate
173          * limit.
174          */
build()175         public MultiRateLimiter build() {
176             return new MultiRateLimiter(mQuotaTrackers);
177         }
178     }
179 
180     /** Helper class that describes a rate limit. */
181     public static class RateLimit {
182         public final int mLimit;
183         public final Duration mWindowSize;
184 
185         /**
186          * @param limit      The maximum count of some occurrence in the rolling time window.
187          * @param windowSize The rolling time window to use when checking quota usage.
188          */
RateLimit(int limit, Duration windowSize)189         private RateLimit(int limit, Duration windowSize) {
190             this.mLimit = limit;
191             this.mWindowSize = windowSize;
192         }
193 
194         /**
195          * @param limit      The maximum count of some occurrence in the rolling time window.
196          * @param windowSize The rolling time window to use when checking quota usage.
197          */
create(int limit, Duration windowSize)198         public static RateLimit create(int limit, Duration windowSize) {
199             return new RateLimit(limit, windowSize);
200         }
201     }
202 }
203