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