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