1 /*
2  * Copyright (C) 2023 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 android.health.connect.ratelimiter;
18 
19 import android.annotation.IntDef;
20 import android.health.connect.HealthConnectException;
21 
22 import com.android.internal.annotations.GuardedBy;
23 
24 import java.lang.annotation.Retention;
25 import java.lang.annotation.RetentionPolicy;
26 import java.time.Duration;
27 import java.time.Instant;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.concurrent.ConcurrentMap;
33 import java.util.concurrent.locks.ReentrantReadWriteLock;
34 import java.util.stream.Collectors;
35 
36 /**
37  * Basic rate limiter that assigns a fixed request rate quota. If no quota has previously been noted
38  * (e.g. first request scenario), the full quota for each window will be immediately granted.
39  *
40  * @hide
41  */
42 public final class RateLimiter {
43     // The maximum number of bytes a client can insert in one go.
44     public static final String CHUNK_SIZE_LIMIT_IN_BYTES = "chunk_size_limit_in_bytes";
45     // The maximum size in bytes of a single record a client can insert in one go.
46     public static final String RECORD_SIZE_LIMIT_IN_BYTES = "record_size_limit_in_bytes";
47     private static final int DEFAULT_API_CALL_COST = 1;
48 
49     private static final ReentrantReadWriteLock sLockAcrossAppQuota = new ReentrantReadWriteLock();
50     private static final Map<Integer, Quota> sQuotaBucketToAcrossAppsRemainingMemoryQuota =
51             new HashMap<>();
52 
53     private static final Map<Integer, Map<Integer, Quota>> sUserIdToQuotasMap = new HashMap<>();
54 
55     private static final ConcurrentMap<Integer, Integer> sLocks = new ConcurrentHashMap<>();
56 
57     private static final Map<Integer, Integer> QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP =
58             new HashMap<>();
59     private static final Map<String, Integer> QUOTA_BUCKET_TO_MAX_MEMORY_QUOTA_MAP =
60             new HashMap<>();
61     private static final ReentrantReadWriteLock sLock = new ReentrantReadWriteLock();
62 
63     @GuardedBy("sLock")
64     private static boolean sRateLimiterEnabled;
65 
66     public static final int QUOTA_BUCKET_READS_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE = 2000;
67     public static final int QUOTA_BUCKET_READS_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE = 16000;
68     public static final int QUOTA_BUCKET_READS_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE = 1000;
69     public static final int QUOTA_BUCKET_READS_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE = 8000;
70     public static final int QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE = 1000;
71     public static final int QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE = 8000;
72     public static final int QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE = 1000;
73     public static final int QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE = 8000;
74     public static final int CHUNK_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE = 5000000;
75     public static final int RECORD_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE = 1000000;
76     public static final int DATA_PUSH_LIMIT_PER_APP_15M_DEFAULT_FLAG_VALUE = 35000000;
77     public static final int DATA_PUSH_LIMIT_ACROSS_APPS_15M_DEFAULT_FLAG_VALUE = 100000000;
78 
79     static {
initQuotaBuckets()80         initQuotaBuckets();
81     }
82 
initQuotaBuckets()83     private static void initQuotaBuckets() {
84         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
85                 QuotaBucket.QUOTA_BUCKET_READS_PER_24H_FOREGROUND,
86                 QUOTA_BUCKET_READS_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE);
87         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
88                 QuotaBucket.QUOTA_BUCKET_READS_PER_24H_BACKGROUND,
89                 QUOTA_BUCKET_READS_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE);
90         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
91                 QuotaBucket.QUOTA_BUCKET_READS_PER_15M_FOREGROUND,
92                 QUOTA_BUCKET_READS_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE);
93         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
94                 QuotaBucket.QUOTA_BUCKET_READS_PER_15M_BACKGROUND,
95                 QUOTA_BUCKET_READS_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE);
96         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
97                 QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND,
98                 QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND_DEFAULT_FLAG_VALUE);
99         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
100                 QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND,
101                 QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND_DEFAULT_FLAG_VALUE);
102         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
103                 QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND,
104                 QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND_DEFAULT_FLAG_VALUE);
105         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
106                 QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND,
107                 QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND_DEFAULT_FLAG_VALUE);
108         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
109                 QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M,
110                 DATA_PUSH_LIMIT_PER_APP_15M_DEFAULT_FLAG_VALUE);
111         QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.put(
112                 QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M,
113                 DATA_PUSH_LIMIT_ACROSS_APPS_15M_DEFAULT_FLAG_VALUE);
114         QUOTA_BUCKET_TO_MAX_MEMORY_QUOTA_MAP.put(
115                 RateLimiter.CHUNK_SIZE_LIMIT_IN_BYTES,
116                 CHUNK_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE);
117         QUOTA_BUCKET_TO_MAX_MEMORY_QUOTA_MAP.put(
118                 RateLimiter.RECORD_SIZE_LIMIT_IN_BYTES,
119                 RECORD_SIZE_LIMIT_IN_BYTES_DEFAULT_FLAG_VALUE);
120     }
121 
122     /** Allows setting lower rate limits in tests. */
setLowerRateLimitsForTesting(boolean enabled)123     public static void setLowerRateLimitsForTesting(boolean enabled) {
124         initQuotaBuckets();
125 
126         if (enabled) {
127             QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.replaceAll((k, v) -> v / 10);
128             QUOTA_BUCKET_TO_MAX_MEMORY_QUOTA_MAP.replaceAll((k, v) -> v / 10);
129         }
130     }
131 
tryAcquireApiCallQuota( int uid, @QuotaCategory.Type int quotaCategory, boolean isInForeground)132     public static void tryAcquireApiCallQuota(
133             int uid, @QuotaCategory.Type int quotaCategory, boolean isInForeground) {
134         sLock.readLock().lock();
135         try {
136             if (!sRateLimiterEnabled) {
137                 return;
138             }
139         } finally {
140             sLock.readLock().unlock();
141         }
142         if (quotaCategory == QuotaCategory.QUOTA_CATEGORY_UNDEFINED) {
143             throw new IllegalArgumentException("Quota category not defined.");
144         }
145 
146         // Rate limiting not applicable.
147         if (quotaCategory == QuotaCategory.QUOTA_CATEGORY_UNMETERED) {
148             return;
149         }
150 
151         synchronized (getLockObject(uid)) {
152             spendApiCallResourcesIfAvailable(
153                     uid,
154                     getAffectedAPIQuotaBuckets(quotaCategory, isInForeground),
155                     DEFAULT_API_CALL_COST);
156         }
157     }
158 
tryAcquireApiCallQuota( int uid, @QuotaCategory.Type int quotaCategory, boolean isInForeground, long memoryCost)159     public static void tryAcquireApiCallQuota(
160             int uid,
161             @QuotaCategory.Type int quotaCategory,
162             boolean isInForeground,
163             long memoryCost) {
164         sLock.readLock().lock();
165         try {
166             if (!sRateLimiterEnabled) {
167                 return;
168             }
169         } finally {
170             sLock.readLock().unlock();
171         }
172         if (quotaCategory == QuotaCategory.QUOTA_CATEGORY_UNDEFINED) {
173             throw new IllegalArgumentException("Quota category not defined.");
174         }
175 
176         // Rate limiting not applicable.
177         if (quotaCategory == QuotaCategory.QUOTA_CATEGORY_UNMETERED) {
178             return;
179         }
180         if (quotaCategory != QuotaCategory.QUOTA_CATEGORY_WRITE) {
181             throw new IllegalArgumentException("Quota category must be QUOTA_CATEGORY_WRITE.");
182         }
183         sLockAcrossAppQuota.writeLock().lock();
184         try {
185             synchronized (getLockObject(uid)) {
186                 spendApiAndMemoryResourcesIfAvailable(
187                         uid,
188                         getAffectedAPIQuotaBuckets(quotaCategory, isInForeground),
189                         getAffectedMemoryQuotaBuckets(quotaCategory, isInForeground),
190                         DEFAULT_API_CALL_COST,
191                         memoryCost,
192                         isInForeground);
193             }
194         } finally {
195             sLockAcrossAppQuota.writeLock().unlock();
196         }
197     }
198 
checkMaxChunkMemoryUsage(long memoryCost)199     public static void checkMaxChunkMemoryUsage(long memoryCost) {
200         sLock.readLock().lock();
201         try {
202             if (!sRateLimiterEnabled) {
203                 return;
204             }
205         } finally {
206             sLock.readLock().unlock();
207         }
208         long memoryLimit = getConfiguredMaxApiMemoryQuota(CHUNK_SIZE_LIMIT_IN_BYTES);
209         if (memoryCost > memoryLimit) {
210             throw new HealthConnectException(
211                     HealthConnectException.ERROR_RATE_LIMIT_EXCEEDED,
212                     "Records chunk size exceeded the max chunk limit: "
213                             + memoryLimit
214                             + ", was: "
215                             + memoryCost);
216         }
217     }
218 
checkMaxRecordMemoryUsage(long memoryCost)219     public static void checkMaxRecordMemoryUsage(long memoryCost) {
220         sLock.readLock().lock();
221         try {
222             if (!sRateLimiterEnabled) {
223                 return;
224             }
225         } finally {
226             sLock.readLock().unlock();
227         }
228         long memoryLimit = getConfiguredMaxApiMemoryQuota(RECORD_SIZE_LIMIT_IN_BYTES);
229         if (memoryCost > memoryLimit) {
230             throw new HealthConnectException(
231                     HealthConnectException.ERROR_RATE_LIMIT_EXCEEDED,
232                     "Record size exceeded the single record size limit: "
233                             + memoryLimit
234                             + ", was: "
235                             + memoryCost);
236         }
237     }
238 
clearCache()239     public static void clearCache() {
240         sUserIdToQuotasMap.clear();
241         sQuotaBucketToAcrossAppsRemainingMemoryQuota.clear();
242     }
243 
updateEnableRateLimiterFlag(boolean enableRateLimiter)244     public static void updateEnableRateLimiterFlag(boolean enableRateLimiter) {
245         sLock.writeLock().lock();
246         try {
247             sRateLimiterEnabled = enableRateLimiter;
248         } finally {
249             sLock.writeLock().unlock();
250         }
251     }
252 
253     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
getLockObject(int uid)254     private static Object getLockObject(int uid) {
255         sLocks.putIfAbsent(uid, uid);
256         return sLocks.get(uid);
257     }
258 
spendApiCallResourcesIfAvailable( int uid, List<Integer> quotaBuckets, int cost)259     private static void spendApiCallResourcesIfAvailable(
260             int uid, List<Integer> quotaBuckets, int cost) {
261         Map<Integer, Float> quotaBucketToAvailableQuotaMap =
262                 getQuotaBucketToAvailableQuotaMap(uid, quotaBuckets);
263         checkIfResourcesAreAvailable(quotaBucketToAvailableQuotaMap, quotaBuckets, cost);
264         spendAvailableResources(uid, quotaBucketToAvailableQuotaMap, quotaBuckets, cost);
265     }
266 
spendApiAndMemoryResourcesIfAvailable( int uid, List<Integer> apiQuotaBuckets, List<Integer> memoryQuotaBuckets, int cost, long memoryCost, boolean isInForeground)267     private static void spendApiAndMemoryResourcesIfAvailable(
268             int uid,
269             List<Integer> apiQuotaBuckets,
270             List<Integer> memoryQuotaBuckets,
271             int cost,
272             long memoryCost,
273             boolean isInForeground) {
274         Map<Integer, Float> apiQuotaBucketToAvailableQuotaMap =
275                 getQuotaBucketToAvailableQuotaMap(uid, apiQuotaBuckets);
276         Map<Integer, Float> memoryQuotaBucketToAvailableQuotaMap =
277                 getQuotaBucketToAvailableQuotaMap(uid, memoryQuotaBuckets);
278         if (!isInForeground) {
279             hasSufficientQuota(
280                     getAvailableQuota(
281                             QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M,
282                             getQuota(QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M)),
283                     memoryCost,
284                     QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M);
285         }
286         checkIfResourcesAreAvailable(apiQuotaBucketToAvailableQuotaMap, apiQuotaBuckets, cost);
287         checkIfResourcesAreAvailable(
288                 memoryQuotaBucketToAvailableQuotaMap, memoryQuotaBuckets, memoryCost);
289         if (!isInForeground) {
290             spendAvailableResources(
291                     getQuota(QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M),
292                     QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M,
293                     memoryCost);
294         }
295         spendAvailableResources(uid, apiQuotaBucketToAvailableQuotaMap, apiQuotaBuckets, cost);
296         spendAvailableResources(
297                 uid, memoryQuotaBucketToAvailableQuotaMap, memoryQuotaBuckets, memoryCost);
298     }
299 
300     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
checkIfResourcesAreAvailable( Map<Integer, Float> quotaBucketToAvailableQuotaMap, List<Integer> quotaBuckets, long cost)301     private static void checkIfResourcesAreAvailable(
302             Map<Integer, Float> quotaBucketToAvailableQuotaMap,
303             List<Integer> quotaBuckets,
304             long cost) {
305         for (@QuotaBucket.Type int quotaBucket : quotaBuckets) {
306             hasSufficientQuota(quotaBucketToAvailableQuotaMap.get(quotaBucket), cost, quotaBucket);
307         }
308     }
309 
spendAvailableResources(Quota quota, Integer quotaBucket, long memoryCost)310     private static void spendAvailableResources(Quota quota, Integer quotaBucket, long memoryCost) {
311         quota.setRemainingQuota(getAvailableQuota(quotaBucket, quota) - memoryCost);
312         quota.setLastUpdatedTime(Instant.now());
313     }
314 
315     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
spendAvailableResources( int uid, Map<Integer, Float> quotaBucketToAvailableQuotaMap, List<Integer> quotaBuckets, long cost)316     private static void spendAvailableResources(
317             int uid,
318             Map<Integer, Float> quotaBucketToAvailableQuotaMap,
319             List<Integer> quotaBuckets,
320             long cost) {
321         for (@QuotaBucket.Type int quotaBucket : quotaBuckets) {
322             spendResources(uid, quotaBucket, quotaBucketToAvailableQuotaMap.get(quotaBucket), cost);
323         }
324     }
325 
326     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
spendResources( int uid, @QuotaBucket.Type int quotaBucket, float availableQuota, long cost)327     private static void spendResources(
328             int uid, @QuotaBucket.Type int quotaBucket, float availableQuota, long cost) {
329         sUserIdToQuotasMap
330                 .get(uid)
331                 .put(quotaBucket, new Quota(Instant.now(), availableQuota - cost));
332     }
333 
getQuotaBucketToAvailableQuotaMap( int uid, List<Integer> quotaBuckets)334     private static Map<Integer, Float> getQuotaBucketToAvailableQuotaMap(
335             int uid, List<Integer> quotaBuckets) {
336         return quotaBuckets.stream()
337                 .collect(
338                         Collectors.toMap(
339                                 quotaBucket -> quotaBucket,
340                                 quotaBucket ->
341                                         getAvailableQuota(
342                                                 quotaBucket, getQuota(uid, quotaBucket))));
343     }
344 
hasSufficientQuota( float availableQuota, long cost, @QuotaBucket.Type int quotaBucket)345     private static void hasSufficientQuota(
346             float availableQuota, long cost, @QuotaBucket.Type int quotaBucket) {
347         if (availableQuota < cost) {
348             throw new RateLimiterException(
349                     "API call quota exceeded, availableQuota: "
350                             + availableQuota
351                             + " requested: "
352                             + cost,
353                     quotaBucket,
354                     getConfiguredMaxRollingQuota(quotaBucket));
355         }
356     }
357 
getAvailableQuota(@uotaBucket.Type int quotaBucket, Quota quota)358     private static float getAvailableQuota(@QuotaBucket.Type int quotaBucket, Quota quota) {
359         Instant lastUpdatedTime = quota.getLastUpdatedTime();
360         Instant currentTime = Instant.now();
361         Duration timeSinceLastQuotaSpend = Duration.between(lastUpdatedTime, currentTime);
362         Duration window = getWindowDuration(quotaBucket);
363         float accumulated =
364                 timeSinceLastQuotaSpend.toMillis()
365                         * (getConfiguredMaxRollingQuota(quotaBucket) / (float) window.toMillis());
366         // Cannot accumulate more than the configured max quota.
367         return Math.min(
368                 quota.getRemainingQuota() + accumulated, getConfiguredMaxRollingQuota(quotaBucket));
369     }
370 
getQuota(int uid, @QuotaBucket.Type int quotaBucket)371     private static Quota getQuota(int uid, @QuotaBucket.Type int quotaBucket) {
372         // Handles first request scenario.
373         if (!sUserIdToQuotasMap.containsKey(uid)) {
374             sUserIdToQuotasMap.put(uid, new HashMap<>());
375         }
376         Map<Integer, Quota> packageQuotas = sUserIdToQuotasMap.get(uid);
377         Quota quota = packageQuotas.get(quotaBucket);
378         if (quota == null) {
379             quota = getInitialQuota(quotaBucket);
380         }
381         return quota;
382     }
383 
getQuota(@uotaBucket.Type int quotaBucket)384     private static Quota getQuota(@QuotaBucket.Type int quotaBucket) {
385         // Handles first request scenario.
386         if (!sQuotaBucketToAcrossAppsRemainingMemoryQuota.containsKey(quotaBucket)) {
387             sQuotaBucketToAcrossAppsRemainingMemoryQuota.put(
388                     quotaBucket,
389                     new Quota(Instant.now(), getConfiguredMaxRollingQuota(quotaBucket)));
390         }
391         return sQuotaBucketToAcrossAppsRemainingMemoryQuota.get(quotaBucket);
392     }
393 
getInitialQuota(@uotaBucket.Type int bucket)394     private static Quota getInitialQuota(@QuotaBucket.Type int bucket) {
395         return new Quota(Instant.now(), getConfiguredMaxRollingQuota(bucket));
396     }
397 
getWindowDuration(@uotaBucket.Type int quotaBucket)398     private static Duration getWindowDuration(@QuotaBucket.Type int quotaBucket) {
399         switch (quotaBucket) {
400             case QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND:
401             case QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND:
402             case QuotaBucket.QUOTA_BUCKET_READS_PER_24H_BACKGROUND:
403             case QuotaBucket.QUOTA_BUCKET_READS_PER_24H_FOREGROUND:
404                 return Duration.ofHours(24);
405             case QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND:
406             case QuotaBucket.QUOTA_BUCKET_READS_PER_15M_FOREGROUND:
407             case QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND:
408             case QuotaBucket.QUOTA_BUCKET_READS_PER_15M_BACKGROUND:
409             case QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M:
410             case QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M:
411                 return Duration.ofMinutes(15);
412             case QuotaBucket.QUOTA_BUCKET_UNDEFINED:
413                 throw new IllegalArgumentException("Invalid quota bucket.");
414         }
415         throw new IllegalArgumentException("Invalid quota bucket.");
416     }
417 
getConfiguredMaxRollingQuota(@uotaBucket.Type int quotaBucket)418     private static float getConfiguredMaxRollingQuota(@QuotaBucket.Type int quotaBucket) {
419         if (!QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.containsKey(quotaBucket)) {
420             throw new IllegalArgumentException(
421                     "Max quota not found for quotaBucket: " + quotaBucket);
422         }
423         return QUOTA_BUCKET_TO_MAX_ROLLING_QUOTA_MAP.get(quotaBucket);
424     }
425 
getConfiguredMaxApiMemoryQuota(String quotaBucket)426     private static int getConfiguredMaxApiMemoryQuota(String quotaBucket) {
427         if (!QUOTA_BUCKET_TO_MAX_MEMORY_QUOTA_MAP.containsKey(quotaBucket)) {
428             throw new IllegalArgumentException(
429                     "Max quota not found for quotaBucket: " + quotaBucket);
430         }
431         return QUOTA_BUCKET_TO_MAX_MEMORY_QUOTA_MAP.get(quotaBucket);
432     }
433 
getAffectedAPIQuotaBuckets( @uotaCategory.Type int quotaCategory, boolean isInForeground)434     private static List<Integer> getAffectedAPIQuotaBuckets(
435             @QuotaCategory.Type int quotaCategory, boolean isInForeground) {
436         switch (quotaCategory) {
437             case QuotaCategory.QUOTA_CATEGORY_READ:
438                 if (isInForeground) {
439                     return List.of(
440                             QuotaBucket.QUOTA_BUCKET_READS_PER_15M_FOREGROUND,
441                             QuotaBucket.QUOTA_BUCKET_READS_PER_24H_FOREGROUND);
442                 } else {
443                     return List.of(
444                             QuotaBucket.QUOTA_BUCKET_READS_PER_15M_BACKGROUND,
445                             QuotaBucket.QUOTA_BUCKET_READS_PER_24H_BACKGROUND);
446                 }
447             case QuotaCategory.QUOTA_CATEGORY_WRITE:
448                 if (isInForeground) {
449                     return List.of(
450                             QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND,
451                             QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND);
452                 } else {
453                     return List.of(
454                             QuotaBucket.QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND,
455                             QuotaBucket.QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND);
456                 }
457             case QuotaCategory.QUOTA_CATEGORY_UNDEFINED:
458             case QuotaCategory.QUOTA_CATEGORY_UNMETERED:
459                 throw new IllegalArgumentException("Invalid quota category.");
460         }
461         throw new IllegalArgumentException("Invalid quota category.");
462     }
463 
getAffectedMemoryQuotaBuckets( @uotaCategory.Type int quotaCategory, boolean isInForeground)464     private static List<Integer> getAffectedMemoryQuotaBuckets(
465             @QuotaCategory.Type int quotaCategory, boolean isInForeground) {
466         switch (quotaCategory) {
467             case QuotaCategory.QUOTA_CATEGORY_WRITE:
468                 if (isInForeground) {
469                     return List.of();
470                 } else {
471                     return List.of(QuotaBucket.QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M);
472                 }
473             case QuotaCategory.QUOTA_CATEGORY_READ:
474             case QuotaCategory.QUOTA_CATEGORY_UNDEFINED:
475             case QuotaCategory.QUOTA_CATEGORY_UNMETERED:
476                 throw new IllegalArgumentException("Invalid quota category.");
477         }
478         throw new IllegalArgumentException("Invalid quota category.");
479     }
480 
481     public static final class QuotaBucket {
482         public static final int QUOTA_BUCKET_UNDEFINED = 0;
483         public static final int QUOTA_BUCKET_READS_PER_15M_FOREGROUND = 1;
484         public static final int QUOTA_BUCKET_READS_PER_24H_FOREGROUND = 2;
485         public static final int QUOTA_BUCKET_READS_PER_15M_BACKGROUND = 3;
486         public static final int QUOTA_BUCKET_READS_PER_24H_BACKGROUND = 4;
487         public static final int QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND = 5;
488         public static final int QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND = 6;
489         public static final int QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND = 7;
490         public static final int QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND = 8;
491         public static final int QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M = 9;
492         public static final int QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M = 10;
493 
QuotaBucket()494         private QuotaBucket() {}
495 
496         /** @hide */
497         @IntDef({
498             QUOTA_BUCKET_UNDEFINED,
499             QUOTA_BUCKET_READS_PER_15M_FOREGROUND,
500             QUOTA_BUCKET_READS_PER_24H_FOREGROUND,
501             QUOTA_BUCKET_READS_PER_15M_BACKGROUND,
502             QUOTA_BUCKET_READS_PER_24H_BACKGROUND,
503             QUOTA_BUCKET_WRITES_PER_15M_FOREGROUND,
504             QUOTA_BUCKET_WRITES_PER_24H_FOREGROUND,
505             QUOTA_BUCKET_WRITES_PER_15M_BACKGROUND,
506             QUOTA_BUCKET_WRITES_PER_24H_BACKGROUND,
507             QUOTA_BUCKET_DATA_PUSH_LIMIT_PER_APP_15M,
508             QUOTA_BUCKET_DATA_PUSH_LIMIT_ACROSS_APPS_15M,
509         })
510         @Retention(RetentionPolicy.SOURCE)
511         public @interface Type {}
512     }
513 
514     public static final class QuotaCategory {
515         public static final int QUOTA_CATEGORY_UNDEFINED = 0;
516         public static final int QUOTA_CATEGORY_UNMETERED = 1;
517         public static final int QUOTA_CATEGORY_READ = 2;
518         public static final int QUOTA_CATEGORY_WRITE = 3;
519 
QuotaCategory()520         private QuotaCategory() {}
521 
522         /** @hide */
523         @IntDef({
524             QUOTA_CATEGORY_UNDEFINED,
525             QUOTA_CATEGORY_UNMETERED,
526             QUOTA_CATEGORY_READ,
527             QUOTA_CATEGORY_WRITE,
528         })
529         @Retention(RetentionPolicy.SOURCE)
530         public @interface Type {}
531     }
532 }
533