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