1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.utils.quota; 18 19 import static android.text.format.DateUtils.MINUTE_IN_MILLIS; 20 21 import static com.android.server.utils.quota.Uptc.string; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.AlarmManager; 26 import android.content.Context; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.Message; 30 import android.util.ArrayMap; 31 import android.util.IndentingPrintWriter; 32 import android.util.LongArrayQueue; 33 import android.util.Slog; 34 import android.util.TimeUtils; 35 import android.util.proto.ProtoOutputStream; 36 import android.util.quota.CountQuotaTrackerProto; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.internal.annotations.VisibleForTesting; 40 41 import java.util.function.Consumer; 42 import java.util.function.Function; 43 44 /** 45 * Class that tracks whether an app has exceeded its defined count quota. 46 * 47 * Quotas are applied per userId-package-tag combination (UPTC). Tags can be null. 48 * 49 * This tracker tracks the count of instantaneous events. 50 * 51 * Limits are applied according to the category the UPTC is placed in. If a UPTC reaches its limit, 52 * it will be considered out of quota until it is below that limit again. A {@link Category} is a 53 * basic construct to apply different limits to different groups of UPTCs. For example, standby 54 * buckets can be a set of categories, or foreground & background could be two categories. If every 55 * UPTC should have the same limits applied, then only one category is needed 56 * ({@see Category.SINGLE_CATEGORY}). 57 * 58 * Note: all limits are enforced per category unless explicitly stated otherwise. 59 * 60 * Test: atest com.android.server.utils.quota.CountQuotaTrackerTest 61 * 62 * @hide 63 */ 64 public class CountQuotaTracker extends QuotaTracker { 65 private static final String TAG = CountQuotaTracker.class.getSimpleName(); 66 private static final boolean DEBUG = false; 67 68 private static final String ALARM_TAG_CLEANUP = "*" + TAG + ".cleanup*"; 69 70 @VisibleForTesting 71 static class ExecutionStats { 72 /** 73 * The time after which this record should be considered invalid (out of date), in the 74 * elapsed realtime timebase. 75 */ 76 public long expirationTimeElapsed; 77 78 /** The window size that's used when counting the number of events. */ 79 public long windowSizeMs; 80 /** The maximum number of events allowed within the window size. */ 81 public int countLimit; 82 83 /** The total number of events that occurred in the window. */ 84 public int countInWindow; 85 86 /** 87 * The time after which the app will be under the category quota again. This is only valid 88 * if {@link #countInWindow} >= {@link #countLimit}. 89 */ 90 public long inQuotaTimeElapsed; 91 92 @Override toString()93 public String toString() { 94 return "expirationTime=" + expirationTimeElapsed + ", " 95 + "windowSizeMs=" + windowSizeMs + ", " 96 + "countLimit=" + countLimit + ", " 97 + "countInWindow=" + countInWindow + ", " 98 + "inQuotaTime=" + inQuotaTimeElapsed; 99 } 100 101 @Override equals(Object obj)102 public boolean equals(Object obj) { 103 if (obj instanceof ExecutionStats) { 104 ExecutionStats other = (ExecutionStats) obj; 105 return this.expirationTimeElapsed == other.expirationTimeElapsed 106 && this.windowSizeMs == other.windowSizeMs 107 && this.countLimit == other.countLimit 108 && this.countInWindow == other.countInWindow 109 && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed; 110 } 111 return false; 112 } 113 114 @Override hashCode()115 public int hashCode() { 116 int result = 0; 117 result = 31 * result + Long.hashCode(expirationTimeElapsed); 118 result = 31 * result + Long.hashCode(windowSizeMs); 119 result = 31 * result + countLimit; 120 result = 31 * result + countInWindow; 121 result = 31 * result + Long.hashCode(inQuotaTimeElapsed); 122 return result; 123 } 124 } 125 126 /** List of times of all instantaneous events for a UPTC, in chronological order. */ 127 // TODO(146148168): introduce a bucketized mode that's more efficient but less accurate 128 @GuardedBy("mLock") 129 private final UptcMap<LongArrayQueue> mEventTimes = new UptcMap<>(); 130 131 /** Cached calculation results for each app. */ 132 @GuardedBy("mLock") 133 private final UptcMap<ExecutionStats> mExecutionStatsCache = new UptcMap<>(); 134 135 private final Handler mHandler; 136 137 @GuardedBy("mLock") 138 private long mNextCleanupTimeElapsed = 0; 139 @GuardedBy("mLock") 140 private final AlarmManager.OnAlarmListener mEventCleanupAlarmListener = () -> 141 CountQuotaTracker.this.mHandler.obtainMessage(MSG_CLEAN_UP_EVENTS).sendToTarget(); 142 143 /** The rolling window size for each Category's count limit. */ 144 @GuardedBy("mLock") 145 private final ArrayMap<Category, Long> mCategoryCountWindowSizesMs = new ArrayMap<>(); 146 147 /** 148 * The maximum count for each Category. For each max value count in the map, the app will 149 * not be allowed any more events within the latest time interval of its rolling window size. 150 * 151 * @see #mCategoryCountWindowSizesMs 152 */ 153 @GuardedBy("mLock") 154 private final ArrayMap<Category, Integer> mMaxCategoryCounts = new ArrayMap<>(); 155 156 /** The longest period a registered category applies to. */ 157 @GuardedBy("mLock") 158 private long mMaxPeriodMs = 0; 159 160 /** Drop any old events. */ 161 private static final int MSG_CLEAN_UP_EVENTS = 1; 162 CountQuotaTracker(@onNull Context context, @NonNull Categorizer categorizer)163 public CountQuotaTracker(@NonNull Context context, @NonNull Categorizer categorizer) { 164 this(context, categorizer, new Injector()); 165 } 166 167 @VisibleForTesting CountQuotaTracker(@onNull Context context, @NonNull Categorizer categorizer, Injector injector)168 CountQuotaTracker(@NonNull Context context, @NonNull Categorizer categorizer, 169 Injector injector) { 170 super(context, categorizer, injector); 171 172 mHandler = new CqtHandler(context.getMainLooper()); 173 } 174 175 // Exposed API to users. 176 177 /** 178 * Record that an instantaneous event happened. 179 * 180 * @return true if the UPTC is within quota, false otherwise. 181 */ noteEvent(int userId, @NonNull String packageName, @Nullable String tag)182 public boolean noteEvent(int userId, @NonNull String packageName, @Nullable String tag) { 183 synchronized (mLock) { 184 if (!isEnabledLocked() || isQuotaFreeLocked(userId, packageName)) { 185 return true; 186 } 187 final long nowElapsed = mInjector.getElapsedRealtime(); 188 189 final LongArrayQueue times = mEventTimes 190 .getOrCreate(userId, packageName, tag, mCreateLongArrayQueue); 191 times.addLast(nowElapsed); 192 final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, tag); 193 stats.countInWindow++; 194 stats.expirationTimeElapsed = Math.min(stats.expirationTimeElapsed, 195 nowElapsed + stats.windowSizeMs); 196 if (stats.countInWindow == stats.countLimit) { 197 final long windowEdgeElapsed = nowElapsed - stats.windowSizeMs; 198 while (times.size() > 0 && times.peekFirst() < windowEdgeElapsed) { 199 times.removeFirst(); 200 } 201 stats.inQuotaTimeElapsed = times.peekFirst() + stats.windowSizeMs; 202 postQuotaStatusChanged(userId, packageName, tag); 203 } else if (stats.countLimit > 9 204 && stats.countInWindow == stats.countLimit * 4 / 5) { 205 // TODO: log high watermark to statsd 206 Slog.w(TAG, string(userId, packageName, tag) 207 + " has reached 80% of it's count limit of " + stats.countLimit); 208 } 209 maybeScheduleCleanupAlarmLocked(); 210 return isWithinQuotaLocked(stats); 211 } 212 } 213 214 /** 215 * Set count limit over a rolling time window for the specified category. 216 * 217 * @param category The category these limits apply to. 218 * @param limit The maximum event count an app can have in the rolling window. Must be 219 * nonnegative. 220 * @param timeWindowMs The rolling time window (in milliseconds) to use when checking quota 221 * usage. Must be at least {@value #MIN_WINDOW_SIZE_MS} and no longer than 222 * {@value #MAX_WINDOW_SIZE_MS} 223 */ setCountLimit(@onNull Category category, int limit, long timeWindowMs)224 public void setCountLimit(@NonNull Category category, int limit, long timeWindowMs) { 225 if (limit < 0 || timeWindowMs < 0) { 226 throw new IllegalArgumentException("Limit and window size must be nonnegative."); 227 } 228 synchronized (mLock) { 229 final Integer oldLimit = mMaxCategoryCounts.put(category, limit); 230 final long newWindowSizeMs = Math.max(MIN_WINDOW_SIZE_MS, 231 Math.min(timeWindowMs, MAX_WINDOW_SIZE_MS)); 232 final Long oldWindowSizeMs = mCategoryCountWindowSizesMs.put(category, newWindowSizeMs); 233 if (oldLimit != null && oldWindowSizeMs != null 234 && oldLimit == limit && oldWindowSizeMs == newWindowSizeMs) { 235 // No change. 236 return; 237 } 238 mDeleteOldEventTimesFunctor.updateMaxPeriod(); 239 mMaxPeriodMs = mDeleteOldEventTimesFunctor.mMaxPeriodMs; 240 invalidateAllExecutionStatsLocked(); 241 } 242 scheduleQuotaCheck(); 243 } 244 245 /** 246 * Gets the count limit for the specified category. 247 */ getLimit(@onNull Category category)248 public int getLimit(@NonNull Category category) { 249 synchronized (mLock) { 250 final Integer limit = mMaxCategoryCounts.get(category); 251 if (limit == null) { 252 throw new IllegalArgumentException("Limit for " + category + " not defined"); 253 } 254 return limit; 255 } 256 } 257 258 /** 259 * Gets the count time window for the specified category. 260 */ getWindowSizeMs(@onNull Category category)261 public long getWindowSizeMs(@NonNull Category category) { 262 synchronized (mLock) { 263 final Long limitMs = mCategoryCountWindowSizesMs.get(category); 264 if (limitMs == null) { 265 throw new IllegalArgumentException("Limit for " + category + " not defined"); 266 } 267 return limitMs; 268 } 269 } 270 271 // Internal implementation. 272 273 @Override 274 @GuardedBy("mLock") dropEverythingLocked()275 void dropEverythingLocked() { 276 mExecutionStatsCache.clear(); 277 mEventTimes.clear(); 278 } 279 280 @Override 281 @GuardedBy("mLock") 282 @NonNull getHandler()283 Handler getHandler() { 284 return mHandler; 285 } 286 287 @Override 288 @GuardedBy("mLock") getInQuotaTimeElapsedLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)289 long getInQuotaTimeElapsedLocked(final int userId, @NonNull final String packageName, 290 @Nullable final String tag) { 291 return getExecutionStatsLocked(userId, packageName, tag).inQuotaTimeElapsed; 292 } 293 294 @Override 295 @GuardedBy("mLock") handleRemovedAppLocked(final int userId, @NonNull String packageName)296 void handleRemovedAppLocked(final int userId, @NonNull String packageName) { 297 if (packageName == null) { 298 Slog.wtf(TAG, "Told app removed but given null package name."); 299 return; 300 } 301 302 mEventTimes.delete(userId, packageName); 303 mExecutionStatsCache.delete(userId, packageName); 304 } 305 306 @Override 307 @GuardedBy("mLock") handleRemovedUserLocked(int userId)308 void handleRemovedUserLocked(int userId) { 309 mEventTimes.delete(userId); 310 mExecutionStatsCache.delete(userId); 311 } 312 313 @Override 314 @GuardedBy("mLock") isWithinQuotaLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)315 boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName, 316 @Nullable final String tag) { 317 if (!isEnabledLocked()) return true; 318 319 // Quota constraint is not enforced when quota is free. 320 if (isQuotaFreeLocked(userId, packageName)) { 321 return true; 322 } 323 324 return isWithinQuotaLocked(getExecutionStatsLocked(userId, packageName, tag)); 325 } 326 327 @Override 328 @GuardedBy("mLock") maybeUpdateAllQuotaStatusLocked()329 void maybeUpdateAllQuotaStatusLocked() { 330 final UptcMap<Boolean> doneMap = new UptcMap<>(); 331 mEventTimes.forEach((userId, packageName, tag, events) -> { 332 if (!doneMap.contains(userId, packageName, tag)) { 333 maybeUpdateStatusForUptcLocked(userId, packageName, tag); 334 doneMap.add(userId, packageName, tag, Boolean.TRUE); 335 } 336 }); 337 338 } 339 340 @Override maybeUpdateQuotaStatus(final int userId, @NonNull final String packageName, @Nullable final String tag)341 void maybeUpdateQuotaStatus(final int userId, @NonNull final String packageName, 342 @Nullable final String tag) { 343 synchronized (mLock) { 344 maybeUpdateStatusForUptcLocked(userId, packageName, tag); 345 } 346 } 347 348 @Override 349 @GuardedBy("mLock") onQuotaFreeChangedLocked(boolean isFree)350 void onQuotaFreeChangedLocked(boolean isFree) { 351 // Nothing to do here. 352 } 353 354 @Override 355 @GuardedBy("mLock") onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree)356 void onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree) { 357 maybeUpdateStatusForPkgLocked(userId, packageName); 358 } 359 360 @GuardedBy("mLock") isWithinQuotaLocked(@onNull final ExecutionStats stats)361 private boolean isWithinQuotaLocked(@NonNull final ExecutionStats stats) { 362 return isUnderCountQuotaLocked(stats); 363 } 364 365 @GuardedBy("mLock") isUnderCountQuotaLocked(@onNull ExecutionStats stats)366 private boolean isUnderCountQuotaLocked(@NonNull ExecutionStats stats) { 367 return stats.countInWindow < stats.countLimit; 368 } 369 370 /** Returns the execution stats of the app in the most recent window. */ 371 @GuardedBy("mLock") 372 @VisibleForTesting 373 @NonNull getExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)374 ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName, 375 @Nullable final String tag) { 376 return getExecutionStatsLocked(userId, packageName, tag, true); 377 } 378 379 @GuardedBy("mLock") 380 @NonNull getExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable String tag, final boolean refreshStatsIfOld)381 private ExecutionStats getExecutionStatsLocked(final int userId, 382 @NonNull final String packageName, @Nullable String tag, 383 final boolean refreshStatsIfOld) { 384 final ExecutionStats stats = 385 mExecutionStatsCache.getOrCreate(userId, packageName, tag, mCreateExecutionStats); 386 if (refreshStatsIfOld) { 387 final Category category = mCategorizer.getCategory(userId, packageName, tag); 388 final long countWindowSizeMs = mCategoryCountWindowSizesMs.getOrDefault(category, 389 Long.MAX_VALUE); 390 final int countLimit = mMaxCategoryCounts.getOrDefault(category, Integer.MAX_VALUE); 391 if (stats.expirationTimeElapsed <= mInjector.getElapsedRealtime() 392 || stats.windowSizeMs != countWindowSizeMs 393 || stats.countLimit != countLimit) { 394 // The stats are no longer valid. 395 stats.windowSizeMs = countWindowSizeMs; 396 stats.countLimit = countLimit; 397 updateExecutionStatsLocked(userId, packageName, tag, stats); 398 } 399 } 400 401 return stats; 402 } 403 404 @GuardedBy("mLock") 405 @VisibleForTesting updateExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable final String tag, @NonNull ExecutionStats stats)406 void updateExecutionStatsLocked(final int userId, @NonNull final String packageName, 407 @Nullable final String tag, @NonNull ExecutionStats stats) { 408 stats.countInWindow = 0; 409 if (stats.countLimit == 0) { 410 // UPTC won't be in quota until configuration changes. 411 stats.inQuotaTimeElapsed = Long.MAX_VALUE; 412 } else { 413 stats.inQuotaTimeElapsed = 0; 414 } 415 416 // This can be used to determine when an app will have enough quota to transition from 417 // out-of-quota to in-quota. 418 final long nowElapsed = mInjector.getElapsedRealtime(); 419 stats.expirationTimeElapsed = nowElapsed + mMaxPeriodMs; 420 421 final LongArrayQueue events = mEventTimes.get(userId, packageName, tag); 422 if (events == null) { 423 return; 424 } 425 426 // The minimum time between the start time and the beginning of the events that were 427 // looked at --> how much time the stats will be valid for. 428 long emptyTimeMs = Long.MAX_VALUE - nowElapsed; 429 430 final long eventStartWindowElapsed = nowElapsed - stats.windowSizeMs; 431 for (int i = events.size() - 1; i >= 0; --i) { 432 final long eventTimeElapsed = events.get(i); 433 if (eventTimeElapsed < eventStartWindowElapsed) { 434 // This event happened before the window. No point in going any further. 435 break; 436 } 437 stats.countInWindow++; 438 emptyTimeMs = Math.min(emptyTimeMs, eventTimeElapsed - eventStartWindowElapsed); 439 440 if (stats.countInWindow >= stats.countLimit) { 441 stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, 442 eventTimeElapsed + stats.windowSizeMs); 443 } 444 } 445 446 stats.expirationTimeElapsed = nowElapsed + emptyTimeMs; 447 } 448 449 /** Invalidate ExecutionStats for all apps. */ 450 @GuardedBy("mLock") invalidateAllExecutionStatsLocked()451 private void invalidateAllExecutionStatsLocked() { 452 final long nowElapsed = mInjector.getElapsedRealtime(); 453 mExecutionStatsCache.forEach((appStats) -> { 454 if (appStats != null) { 455 appStats.expirationTimeElapsed = nowElapsed; 456 } 457 }); 458 } 459 460 @GuardedBy("mLock") invalidateAllExecutionStatsLocked(final int userId, @NonNull final String packageName)461 private void invalidateAllExecutionStatsLocked(final int userId, 462 @NonNull final String packageName) { 463 final ArrayMap<String, ExecutionStats> appStats = 464 mExecutionStatsCache.get(userId, packageName); 465 if (appStats != null) { 466 final long nowElapsed = mInjector.getElapsedRealtime(); 467 final int numStats = appStats.size(); 468 for (int i = 0; i < numStats; ++i) { 469 final ExecutionStats stats = appStats.valueAt(i); 470 if (stats != null) { 471 stats.expirationTimeElapsed = nowElapsed; 472 } 473 } 474 } 475 } 476 477 @GuardedBy("mLock") invalidateExecutionStatsLocked(final int userId, @NonNull final String packageName, @Nullable String tag)478 private void invalidateExecutionStatsLocked(final int userId, @NonNull final String packageName, 479 @Nullable String tag) { 480 final ExecutionStats stats = mExecutionStatsCache.get(userId, packageName, tag); 481 if (stats != null) { 482 stats.expirationTimeElapsed = mInjector.getElapsedRealtime(); 483 } 484 } 485 486 private static final class EarliestEventTimeFunctor implements Consumer<LongArrayQueue> { 487 long earliestTimeElapsed = Long.MAX_VALUE; 488 489 @Override accept(LongArrayQueue events)490 public void accept(LongArrayQueue events) { 491 if (events != null && events.size() > 0) { 492 earliestTimeElapsed = Math.min(earliestTimeElapsed, events.get(0)); 493 } 494 } 495 reset()496 void reset() { 497 earliestTimeElapsed = Long.MAX_VALUE; 498 } 499 } 500 501 private final EarliestEventTimeFunctor mEarliestEventTimeFunctor = 502 new EarliestEventTimeFunctor(); 503 504 /** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */ 505 @GuardedBy("mLock") 506 @VisibleForTesting maybeScheduleCleanupAlarmLocked()507 void maybeScheduleCleanupAlarmLocked() { 508 if (mNextCleanupTimeElapsed > mInjector.getElapsedRealtime()) { 509 // There's already an alarm scheduled. Just stick with that one. There's no way we'll 510 // end up scheduling an earlier alarm. 511 if (DEBUG) { 512 Slog.v(TAG, "Not scheduling cleanup since there's already one at " 513 + mNextCleanupTimeElapsed + " (in " + (mNextCleanupTimeElapsed 514 - mInjector.getElapsedRealtime()) + "ms)"); 515 } 516 return; 517 } 518 519 mEarliestEventTimeFunctor.reset(); 520 mEventTimes.forEach(mEarliestEventTimeFunctor); 521 final long earliestEndElapsed = mEarliestEventTimeFunctor.earliestTimeElapsed; 522 if (earliestEndElapsed == Long.MAX_VALUE) { 523 // Couldn't find a good time to clean up. Maybe this was called after we deleted all 524 // events. 525 if (DEBUG) { 526 Slog.d(TAG, "Didn't find a time to schedule cleanup"); 527 } 528 return; 529 } 530 531 // Need to keep events for all apps up to the max period, regardless of their current 532 // category. 533 long nextCleanupElapsed = earliestEndElapsed + mMaxPeriodMs; 534 if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) { 535 // No need to clean up too often. Delay the alarm if the next cleanup would be too soon 536 // after it. 537 nextCleanupElapsed += 10 * MINUTE_IN_MILLIS; 538 } 539 mNextCleanupTimeElapsed = nextCleanupElapsed; 540 scheduleAlarm(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP, 541 mEventCleanupAlarmListener); 542 if (DEBUG) { 543 Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed); 544 } 545 } 546 547 @GuardedBy("mLock") maybeUpdateStatusForPkgLocked(final int userId, @NonNull final String packageName)548 private boolean maybeUpdateStatusForPkgLocked(final int userId, 549 @NonNull final String packageName) { 550 final UptcMap<Boolean> done = new UptcMap<>(); 551 552 if (!mEventTimes.contains(userId, packageName)) { 553 return false; 554 } 555 final ArrayMap<String, LongArrayQueue> events = mEventTimes.get(userId, packageName); 556 if (events == null) { 557 Slog.wtf(TAG, 558 "Events map was null even though mEventTimes said it contained " 559 + string(userId, packageName, null)); 560 return false; 561 } 562 563 // Lambdas can't interact with non-final outer variables. 564 final boolean[] changed = {false}; 565 events.forEach((tag, eventList) -> { 566 if (!done.contains(userId, packageName, tag)) { 567 changed[0] |= maybeUpdateStatusForUptcLocked(userId, packageName, tag); 568 done.add(userId, packageName, tag, Boolean.TRUE); 569 } 570 }); 571 572 return changed[0]; 573 } 574 575 /** 576 * Posts that the quota status for the UPTC has changed if it has changed. Avoid calling if 577 * there are no {@link QuotaChangeListener}s registered as the work done will be useless. 578 * 579 * @return true if the in/out quota status changed 580 */ 581 @GuardedBy("mLock") maybeUpdateStatusForUptcLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)582 private boolean maybeUpdateStatusForUptcLocked(final int userId, 583 @NonNull final String packageName, @Nullable final String tag) { 584 final boolean oldInQuota = isWithinQuotaLocked( 585 getExecutionStatsLocked(userId, packageName, tag, false)); 586 587 final boolean newInQuota; 588 if (!isEnabledLocked() || isQuotaFreeLocked(userId, packageName)) { 589 newInQuota = true; 590 } else { 591 newInQuota = isWithinQuotaLocked( 592 getExecutionStatsLocked(userId, packageName, tag, true)); 593 } 594 595 if (!newInQuota) { 596 maybeScheduleStartAlarmLocked(userId, packageName, tag); 597 } else { 598 cancelScheduledStartAlarmLocked(userId, packageName, tag); 599 } 600 601 if (oldInQuota != newInQuota) { 602 if (DEBUG) { 603 Slog.d(TAG, 604 "Quota status changed from " + oldInQuota + " to " + newInQuota + " for " 605 + string(userId, packageName, tag)); 606 } 607 postQuotaStatusChanged(userId, packageName, tag); 608 return true; 609 } 610 611 return false; 612 } 613 614 private final class DeleteEventTimesFunctor implements Consumer<LongArrayQueue> { 615 private long mMaxPeriodMs; 616 617 @Override accept(LongArrayQueue times)618 public void accept(LongArrayQueue times) { 619 if (times != null) { 620 // Remove everything older than mMaxPeriodMs time ago. 621 while (times.size() > 0 622 && times.peekFirst() <= mInjector.getElapsedRealtime() - mMaxPeriodMs) { 623 times.removeFirst(); 624 } 625 } 626 } 627 updateMaxPeriod()628 private void updateMaxPeriod() { 629 long maxPeriodMs = 0; 630 for (int i = mCategoryCountWindowSizesMs.size() - 1; i >= 0; --i) { 631 maxPeriodMs = Long.max(maxPeriodMs, mCategoryCountWindowSizesMs.valueAt(i)); 632 } 633 mMaxPeriodMs = maxPeriodMs; 634 } 635 } 636 637 private final DeleteEventTimesFunctor mDeleteOldEventTimesFunctor = 638 new DeleteEventTimesFunctor(); 639 640 @GuardedBy("mLock") 641 @VisibleForTesting deleteObsoleteEventsLocked()642 void deleteObsoleteEventsLocked() { 643 mEventTimes.forEach(mDeleteOldEventTimesFunctor); 644 } 645 646 private class CqtHandler extends Handler { CqtHandler(Looper looper)647 CqtHandler(Looper looper) { 648 super(looper); 649 } 650 651 @Override handleMessage(Message msg)652 public void handleMessage(Message msg) { 653 synchronized (mLock) { 654 switch (msg.what) { 655 case MSG_CLEAN_UP_EVENTS: { 656 if (DEBUG) { 657 Slog.d(TAG, "Cleaning up events."); 658 } 659 deleteObsoleteEventsLocked(); 660 maybeScheduleCleanupAlarmLocked(); 661 break; 662 } 663 } 664 } 665 } 666 } 667 668 private Function<Void, LongArrayQueue> mCreateLongArrayQueue = aVoid -> new LongArrayQueue(); 669 private Function<Void, ExecutionStats> mCreateExecutionStats = aVoid -> new ExecutionStats(); 670 671 //////////////////////// TESTING HELPERS ///////////////////////////// 672 673 @VisibleForTesting 674 @Nullable getEvents(int userId, String packageName, String tag)675 LongArrayQueue getEvents(int userId, String packageName, String tag) { 676 return mEventTimes.get(userId, packageName, tag); 677 } 678 679 //////////////////////////// DATA DUMP ////////////////////////////// 680 681 /** Dump state in text format. */ dump(final IndentingPrintWriter pw)682 public void dump(final IndentingPrintWriter pw) { 683 pw.print(TAG); 684 pw.println(":"); 685 pw.increaseIndent(); 686 687 synchronized (mLock) { 688 super.dump(pw); 689 pw.println(); 690 691 pw.println("Instantaneous events:"); 692 pw.increaseIndent(); 693 mEventTimes.forEach((userId, pkgName, tag, events) -> { 694 if (events.size() > 0) { 695 pw.print(string(userId, pkgName, tag)); 696 pw.println(":"); 697 pw.increaseIndent(); 698 pw.print(events.get(0)); 699 for (int i = 1; i < events.size(); ++i) { 700 pw.print(", "); 701 pw.print(events.get(i)); 702 } 703 pw.decreaseIndent(); 704 pw.println(); 705 } 706 }); 707 pw.decreaseIndent(); 708 709 pw.println(); 710 pw.println("Cached execution stats:"); 711 pw.increaseIndent(); 712 mExecutionStatsCache.forEach((userId, pkgName, tag, stats) -> { 713 if (stats != null) { 714 pw.print(string(userId, pkgName, tag)); 715 pw.println(":"); 716 pw.increaseIndent(); 717 pw.println(stats); 718 pw.decreaseIndent(); 719 } 720 }); 721 pw.decreaseIndent(); 722 723 pw.println(); 724 pw.println("Limits:"); 725 pw.increaseIndent(); 726 final int numCategories = mCategoryCountWindowSizesMs.size(); 727 for (int i = 0; i < numCategories; ++i) { 728 final Category category = mCategoryCountWindowSizesMs.keyAt(i); 729 pw.print(category); 730 pw.print(": "); 731 pw.print(mMaxCategoryCounts.get(category)); 732 pw.print(" events in "); 733 pw.println(TimeUtils.formatDuration(mCategoryCountWindowSizesMs.get(category))); 734 } 735 pw.decreaseIndent(); 736 } 737 pw.decreaseIndent(); 738 } 739 740 /** 741 * Dump state to proto. 742 * 743 * @param proto The ProtoOutputStream to write to. 744 * @param fieldId The field ID of the {@link CountQuotaTrackerProto}. 745 */ dump(ProtoOutputStream proto, long fieldId)746 public void dump(ProtoOutputStream proto, long fieldId) { 747 final long token = proto.start(fieldId); 748 749 synchronized (mLock) { 750 super.dump(proto, CountQuotaTrackerProto.BASE_QUOTA_DATA); 751 752 for (int i = 0; i < mCategoryCountWindowSizesMs.size(); ++i) { 753 final Category category = mCategoryCountWindowSizesMs.keyAt(i); 754 final long clToken = proto.start(CountQuotaTrackerProto.COUNT_LIMIT); 755 category.dumpDebug(proto, CountQuotaTrackerProto.CountLimit.CATEGORY); 756 proto.write(CountQuotaTrackerProto.CountLimit.LIMIT, 757 mMaxCategoryCounts.get(category)); 758 proto.write(CountQuotaTrackerProto.CountLimit.WINDOW_SIZE_MS, 759 mCategoryCountWindowSizesMs.get(category)); 760 proto.end(clToken); 761 } 762 763 mExecutionStatsCache.forEach((userId, pkgName, tag, stats) -> { 764 final boolean isQuotaFree = isIndividualQuotaFreeLocked(userId, pkgName); 765 766 final long usToken = proto.start(CountQuotaTrackerProto.UPTC_STATS); 767 768 (new Uptc(userId, pkgName, tag)) 769 .dumpDebug(proto, CountQuotaTrackerProto.UptcStats.UPTC); 770 771 proto.write(CountQuotaTrackerProto.UptcStats.IS_QUOTA_FREE, isQuotaFree); 772 773 final LongArrayQueue events = mEventTimes.get(userId, pkgName, tag); 774 if (events != null) { 775 for (int j = events.size() - 1; j >= 0; --j) { 776 final long eToken = proto.start(CountQuotaTrackerProto.UptcStats.EVENTS); 777 proto.write(CountQuotaTrackerProto.Event.TIMESTAMP_ELAPSED, events.get(j)); 778 proto.end(eToken); 779 } 780 } 781 782 final long statsToken = proto.start( 783 CountQuotaTrackerProto.UptcStats.EXECUTION_STATS); 784 proto.write( 785 CountQuotaTrackerProto.ExecutionStats.EXPIRATION_TIME_ELAPSED, 786 stats.expirationTimeElapsed); 787 proto.write( 788 CountQuotaTrackerProto.ExecutionStats.WINDOW_SIZE_MS, 789 stats.windowSizeMs); 790 proto.write(CountQuotaTrackerProto.ExecutionStats.COUNT_LIMIT, stats.countLimit); 791 proto.write( 792 CountQuotaTrackerProto.ExecutionStats.COUNT_IN_WINDOW, 793 stats.countInWindow); 794 proto.write( 795 CountQuotaTrackerProto.ExecutionStats.IN_QUOTA_TIME_ELAPSED, 796 stats.inQuotaTimeElapsed); 797 proto.end(statsToken); 798 799 proto.end(usToken); 800 }); 801 802 proto.end(token); 803 } 804 } 805 } 806