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