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.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.os.SystemClock; 33 import android.os.UserHandle; 34 import android.util.ArraySet; 35 import android.util.IndentingPrintWriter; 36 import android.util.Pair; 37 import android.util.Slog; 38 import android.util.SparseArrayMap; 39 import android.util.proto.ProtoOutputStream; 40 import android.util.quota.QuotaTrackerProto; 41 42 import com.android.internal.annotations.GuardedBy; 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.os.BackgroundThread; 45 import com.android.server.FgThread; 46 import com.android.server.LocalServices; 47 import com.android.server.SystemServiceManager; 48 49 import java.util.PriorityQueue; 50 51 /** 52 * Base class for trackers that track whether an app has exceeded a count quota. 53 * 54 * Quotas are applied per userId-package-tag combination (UPTC). Tags can be null. 55 * 56 * Count and duration limits can be applied at the same time. Each limit is evaluated and 57 * controlled independently. If a UPTC reaches one of the limits, it will be considered out 58 * of quota until it is below that limit again. Limits are applied according to the category 59 * the UPTC is placed in. Categories are basic constructs to apply different limits to 60 * different groups of UPTCs. For example, standby buckets can be a set of categories, or 61 * foreground & background could be two categories. If every UPTC should have the same limits 62 * applied, then only one category is needed. 63 * 64 * Note: all limits are enforced per category unless explicitly stated otherwise. 65 * 66 * @hide 67 */ 68 abstract class QuotaTracker { 69 private static final String TAG = QuotaTracker.class.getSimpleName(); 70 private static final boolean DEBUG = false; 71 72 private static final String ALARM_TAG_QUOTA_CHECK = "*" + TAG + ".quota_check*"; 73 74 @VisibleForTesting 75 static class Injector { getElapsedRealtime()76 long getElapsedRealtime() { 77 return SystemClock.elapsedRealtime(); 78 } 79 isAlarmManagerReady()80 boolean isAlarmManagerReady() { 81 return LocalServices.getService(SystemServiceManager.class).isBootCompleted(); 82 } 83 } 84 85 final Object mLock = new Object(); 86 final Categorizer mCategorizer; 87 @GuardedBy("mLock") 88 private final ArraySet<QuotaChangeListener> mQuotaChangeListeners = new ArraySet<>(); 89 90 /** 91 * Listener to track and manage when each package comes back within quota. 92 */ 93 @GuardedBy("mLock") 94 private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener(); 95 96 /** "Free quota status" for apps. */ 97 @GuardedBy("mLock") 98 private final SparseArrayMap<String, Boolean> mFreeQuota = new SparseArrayMap<>(); 99 100 private final AlarmManager mAlarmManager; 101 protected final Context mContext; 102 protected final Injector mInjector; 103 104 @GuardedBy("mLock") 105 private boolean mIsQuotaFree; 106 107 /** 108 * If QuotaTracker should actively track events and check quota. If false, quota will be free 109 * and events will not be tracked. 110 */ 111 @GuardedBy("mLock") 112 private boolean mIsEnabled = true; 113 114 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 115 private String getPackageName(Intent intent) { 116 final Uri uri = intent.getData(); 117 return uri != null ? uri.getSchemeSpecificPart() : null; 118 } 119 120 @Override 121 public void onReceive(Context context, Intent intent) { 122 if (intent == null 123 || intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { 124 return; 125 } 126 final String action = intent.getAction(); 127 if (action == null) { 128 Slog.e(TAG, "Received intent with null action"); 129 return; 130 } 131 switch (action) { 132 case Intent.ACTION_PACKAGE_FULLY_REMOVED: 133 final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); 134 synchronized (mLock) { 135 onAppRemovedLocked(UserHandle.getUserId(uid), getPackageName(intent)); 136 } 137 break; 138 case Intent.ACTION_USER_REMOVED: 139 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); 140 synchronized (mLock) { 141 onUserRemovedLocked(userId); 142 } 143 break; 144 } 145 } 146 }; 147 148 /** The maximum period any Category can have. */ 149 @VisibleForTesting 150 static final long MAX_WINDOW_SIZE_MS = 30 * 24 * 60 * MINUTE_IN_MILLIS; // 1 month 151 152 /** 153 * The minimum time any window size can be. A minimum window size helps to avoid CPU 154 * churn/looping in cases where there are registered listeners for when UPTCs go in and out of 155 * quota. 156 */ 157 @VisibleForTesting 158 static final long MIN_WINDOW_SIZE_MS = 20_000; 159 QuotaTracker(@onNull Context context, @NonNull Categorizer categorizer, @NonNull Injector injector)160 QuotaTracker(@NonNull Context context, @NonNull Categorizer categorizer, 161 @NonNull Injector injector) { 162 mCategorizer = categorizer; 163 mContext = context; 164 mInjector = injector; 165 mAlarmManager = mContext.getSystemService(AlarmManager.class); 166 167 final IntentFilter filter = new IntentFilter(); 168 filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); 169 filter.addDataScheme("package"); 170 context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, 171 BackgroundThread.getHandler()); 172 final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED); 173 context.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, userFilter, null, 174 BackgroundThread.getHandler()); 175 } 176 177 // Exposed API to users. 178 179 /** Remove all saved events from the tracker. */ clear()180 public void clear() { 181 synchronized (mLock) { 182 mInQuotaAlarmListener.clearLocked(); 183 mFreeQuota.clear(); 184 185 dropEverythingLocked(); 186 } 187 } 188 189 /** 190 * @return true if the UPTC is within quota, false otherwise. 191 * @throws IllegalStateException if given categorizer returns a Category that's not recognized. 192 */ isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag)193 public boolean isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag) { 194 synchronized (mLock) { 195 return isWithinQuotaLocked(userId, packageName, tag); 196 } 197 } 198 199 /** 200 * Indicates whether quota is currently free or not for a specific app. If quota is free, any 201 * currently ongoing events or instantaneous events won't be counted until quota is no longer 202 * free. 203 */ setQuotaFree(int userId, @NonNull String packageName, boolean isFree)204 public void setQuotaFree(int userId, @NonNull String packageName, boolean isFree) { 205 synchronized (mLock) { 206 final boolean wasFree = mFreeQuota.getOrDefault(userId, packageName, Boolean.FALSE); 207 if (wasFree != isFree) { 208 mFreeQuota.add(userId, packageName, isFree); 209 onQuotaFreeChangedLocked(userId, packageName, isFree); 210 } 211 } 212 } 213 214 /** Indicates whether quota is currently free or not for all apps. */ setQuotaFree(boolean isFree)215 public void setQuotaFree(boolean isFree) { 216 synchronized (mLock) { 217 if (mIsQuotaFree == isFree) { 218 return; 219 } 220 mIsQuotaFree = isFree; 221 222 if (!mIsEnabled) { 223 return; 224 } 225 onQuotaFreeChangedLocked(mIsQuotaFree); 226 } 227 scheduleQuotaCheck(); 228 } 229 230 /** 231 * Register a {@link QuotaChangeListener} to be notified of when apps go in and out of quota. 232 */ registerQuotaChangeListener(QuotaChangeListener listener)233 public void registerQuotaChangeListener(QuotaChangeListener listener) { 234 synchronized (mLock) { 235 if (mQuotaChangeListeners.add(listener) && mQuotaChangeListeners.size() == 1) { 236 scheduleQuotaCheck(); 237 } 238 } 239 } 240 241 /** Unregister the listener from future quota change notifications. */ unregisterQuotaChangeListener(QuotaChangeListener listener)242 public void unregisterQuotaChangeListener(QuotaChangeListener listener) { 243 synchronized (mLock) { 244 mQuotaChangeListeners.remove(listener); 245 } 246 } 247 248 // Configuration APIs 249 250 /** 251 * Completely enables or disables the quota tracker. If the tracker is disabled, all events and 252 * internal tracking data will be dropped. 253 */ setEnabled(boolean enable)254 public void setEnabled(boolean enable) { 255 synchronized (mLock) { 256 if (mIsEnabled == enable) { 257 return; 258 } 259 mIsEnabled = enable; 260 261 if (!mIsEnabled) { 262 clear(); 263 } 264 } 265 } 266 267 // Internal implementation. 268 269 @GuardedBy("mLock") isEnabledLocked()270 boolean isEnabledLocked() { 271 return mIsEnabled; 272 } 273 274 /** Returns true if global quota is free. */ 275 @GuardedBy("mLock") isQuotaFreeLocked()276 boolean isQuotaFreeLocked() { 277 return mIsQuotaFree; 278 } 279 280 /** Returns true if global quota is free or if quota is free for the given userId-package. */ 281 @GuardedBy("mLock") isQuotaFreeLocked(int userId, @NonNull String packageName)282 boolean isQuotaFreeLocked(int userId, @NonNull String packageName) { 283 return mIsQuotaFree || mFreeQuota.getOrDefault(userId, packageName, Boolean.FALSE); 284 } 285 286 /** 287 * Returns true only if quota is free for the given userId-package. Global quota is not taken 288 * into account. 289 */ 290 @GuardedBy("mLock") isIndividualQuotaFreeLocked(int userId, @NonNull String packageName)291 boolean isIndividualQuotaFreeLocked(int userId, @NonNull String packageName) { 292 return mFreeQuota.getOrDefault(userId, packageName, Boolean.FALSE); 293 } 294 295 /** The tracker has been disabled. Drop all events and internal tracking data. */ 296 @GuardedBy("mLock") dropEverythingLocked()297 abstract void dropEverythingLocked(); 298 299 /** The global free quota status changed. */ 300 @GuardedBy("mLock") onQuotaFreeChangedLocked(boolean isFree)301 abstract void onQuotaFreeChangedLocked(boolean isFree); 302 303 /** The individual free quota status for the userId-package changed. */ 304 @GuardedBy("mLock") onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree)305 abstract void onQuotaFreeChangedLocked(int userId, @NonNull String packageName, boolean isFree); 306 307 /** Get the Handler used by the tracker. This Handler's thread will receive alarm callbacks. */ 308 @NonNull getHandler()309 abstract Handler getHandler(); 310 311 /** Makes sure to call out to AlarmManager on a separate thread. */ scheduleAlarm(@larmManager.AlarmType int type, long triggerAtMillis, String tag, AlarmManager.OnAlarmListener listener)312 void scheduleAlarm(@AlarmManager.AlarmType int type, long triggerAtMillis, String tag, 313 AlarmManager.OnAlarmListener listener) { 314 // We don't know at what level in the lock hierarchy this tracker will be, so make sure to 315 // call out to AlarmManager without the lock held. The operation should be fast enough so 316 // put it on the FgThread. 317 FgThread.getHandler().post(() -> { 318 if (mInjector.isAlarmManagerReady()) { 319 mAlarmManager.set(type, triggerAtMillis, tag, listener, getHandler()); 320 } else { 321 Slog.w(TAG, "Alarm not scheduled because boot isn't completed"); 322 } 323 }); 324 } 325 326 /** Makes sure to call out to AlarmManager on a separate thread. */ cancelAlarm(AlarmManager.OnAlarmListener listener)327 void cancelAlarm(AlarmManager.OnAlarmListener listener) { 328 // We don't know at what level in the lock hierarchy this tracker will be, so make sure to 329 // call out to AlarmManager without the lock held. The operation should be fast enough so 330 // put it on the FgThread. 331 FgThread.getHandler().post(() -> { 332 if (mInjector.isAlarmManagerReady()) { 333 mAlarmManager.cancel(listener); 334 } else { 335 Slog.w(TAG, "Alarm not cancelled because boot isn't completed"); 336 } 337 }); 338 } 339 340 /** Check the quota status of the specific UPTC. */ maybeUpdateQuotaStatus(int userId, @NonNull String packageName, @Nullable String tag)341 abstract void maybeUpdateQuotaStatus(int userId, @NonNull String packageName, 342 @Nullable String tag); 343 344 /** Check the quota status of all UPTCs in case a listener needs to be notified. */ 345 @GuardedBy("mLock") maybeUpdateAllQuotaStatusLocked()346 abstract void maybeUpdateAllQuotaStatusLocked(); 347 348 /** Schedule a quota check for all apps. */ scheduleQuotaCheck()349 void scheduleQuotaCheck() { 350 // Using BackgroundThread because of the risk of lock contention. 351 BackgroundThread.getHandler().post(() -> { 352 synchronized (mLock) { 353 if (mQuotaChangeListeners.size() > 0) { 354 maybeUpdateAllQuotaStatusLocked(); 355 } 356 } 357 }); 358 } 359 360 @GuardedBy("mLock") handleRemovedAppLocked(int userId, @NonNull String packageName)361 abstract void handleRemovedAppLocked(int userId, @NonNull String packageName); 362 363 @GuardedBy("mLock") onAppRemovedLocked(final int userId, @NonNull String packageName)364 void onAppRemovedLocked(final int userId, @NonNull String packageName) { 365 if (packageName == null) { 366 Slog.wtf(TAG, "Told app removed but given null package name."); 367 return; 368 } 369 370 mInQuotaAlarmListener.removeAlarmsLocked(userId, packageName); 371 372 mFreeQuota.delete(userId, packageName); 373 374 handleRemovedAppLocked(userId, packageName); 375 } 376 377 @GuardedBy("mLock") handleRemovedUserLocked(int userId)378 abstract void handleRemovedUserLocked(int userId); 379 380 @GuardedBy("mLock") onUserRemovedLocked(int userId)381 private void onUserRemovedLocked(int userId) { 382 mInQuotaAlarmListener.removeAlarmsLocked(userId); 383 mFreeQuota.delete(userId); 384 385 handleRemovedUserLocked(userId); 386 } 387 388 @GuardedBy("mLock") isWithinQuotaLocked(int userId, @NonNull String packageName, @Nullable String tag)389 abstract boolean isWithinQuotaLocked(int userId, @NonNull String packageName, 390 @Nullable String tag); 391 postQuotaStatusChanged(final int userId, @NonNull final String packageName, @Nullable final String tag)392 void postQuotaStatusChanged(final int userId, @NonNull final String packageName, 393 @Nullable final String tag) { 394 BackgroundThread.getHandler().post(() -> { 395 final QuotaChangeListener[] listeners; 396 synchronized (mLock) { 397 // Only notify all listeners if we aren't directing to one listener. 398 listeners = mQuotaChangeListeners.toArray( 399 new QuotaChangeListener[mQuotaChangeListeners.size()]); 400 } 401 for (QuotaChangeListener listener : listeners) { 402 listener.onQuotaStateChanged(userId, packageName, tag); 403 } 404 }); 405 } 406 407 /** 408 * Return the time (in the elapsed realtime timebase) when the UPTC will have quota again. This 409 * value is only valid if the UPTC is currently out of quota. 410 */ 411 @GuardedBy("mLock") getInQuotaTimeElapsedLocked(int userId, @NonNull String packageName, @Nullable String tag)412 abstract long getInQuotaTimeElapsedLocked(int userId, @NonNull String packageName, 413 @Nullable String tag); 414 415 /** 416 * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run 417 * again. This should only be called if the package is already out of quota. 418 */ 419 @GuardedBy("mLock") 420 @VisibleForTesting maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)421 void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName, 422 @Nullable final String tag) { 423 if (mQuotaChangeListeners.size() == 0) { 424 // No need to schedule the alarm since we won't do anything when the app gets quota 425 // again. 426 return; 427 } 428 429 final String pkgString = string(userId, packageName, tag); 430 431 if (isWithinQuota(userId, packageName, tag)) { 432 // Already in quota. Why was this method called? 433 if (DEBUG) { 434 Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString 435 + " even though it's within quota"); 436 } 437 mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag)); 438 maybeUpdateQuotaStatus(userId, packageName, tag); 439 return; 440 } 441 442 mInQuotaAlarmListener.addAlarmLocked(new Uptc(userId, packageName, tag), 443 getInQuotaTimeElapsedLocked(userId, packageName, tag)); 444 } 445 446 @GuardedBy("mLock") cancelScheduledStartAlarmLocked(final int userId, @NonNull final String packageName, @Nullable final String tag)447 void cancelScheduledStartAlarmLocked(final int userId, 448 @NonNull final String packageName, @Nullable final String tag) { 449 mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag)); 450 } 451 452 static class AlarmQueue extends PriorityQueue<Pair<Uptc, Long>> { AlarmQueue()453 AlarmQueue() { 454 super(1, (o1, o2) -> (int) (o1.second - o2.second)); 455 } 456 457 /** 458 * Remove any instances of the Uptc from the queue. 459 * 460 * @return true if an instance was removed, false otherwise. 461 */ remove(@onNull Uptc uptc)462 boolean remove(@NonNull Uptc uptc) { 463 boolean removed = false; 464 Pair[] alarms = toArray(new Pair[size()]); 465 for (int i = alarms.length - 1; i >= 0; --i) { 466 if (uptc.equals(alarms[i].first)) { 467 remove(alarms[i]); 468 removed = true; 469 } 470 } 471 return removed; 472 } 473 } 474 475 /** Track when UPTCs are expected to come back into quota. */ 476 private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener { 477 @GuardedBy("mLock") 478 private final AlarmQueue mAlarmQueue = new AlarmQueue(); 479 /** The next time the alarm is set to go off, in the elapsed realtime timebase. */ 480 @GuardedBy("mLock") 481 private long mTriggerTimeElapsed = 0; 482 483 @GuardedBy("mLock") addAlarmLocked(@onNull Uptc uptc, long inQuotaTimeElapsed)484 void addAlarmLocked(@NonNull Uptc uptc, long inQuotaTimeElapsed) { 485 mAlarmQueue.remove(uptc); 486 mAlarmQueue.offer(new Pair<>(uptc, inQuotaTimeElapsed)); 487 setNextAlarmLocked(); 488 } 489 490 @GuardedBy("mLock") clearLocked()491 void clearLocked() { 492 cancelAlarm(this); 493 mAlarmQueue.clear(); 494 mTriggerTimeElapsed = 0; 495 } 496 497 @GuardedBy("mLock") removeAlarmLocked(@onNull Uptc uptc)498 void removeAlarmLocked(@NonNull Uptc uptc) { 499 if (mAlarmQueue.remove(uptc)) { 500 if (mAlarmQueue.size() == 0) { 501 cancelAlarm(this); 502 } else { 503 setNextAlarmLocked(); 504 } 505 } 506 } 507 508 @GuardedBy("mLock") removeAlarmsLocked(int userId)509 void removeAlarmsLocked(int userId) { 510 boolean removed = false; 511 Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); 512 for (int i = alarms.length - 1; i >= 0; --i) { 513 final Uptc uptc = (Uptc) alarms[i].first; 514 if (userId == uptc.userId) { 515 mAlarmQueue.remove(alarms[i]); 516 removed = true; 517 } 518 } 519 if (removed) { 520 setNextAlarmLocked(); 521 } 522 } 523 524 @GuardedBy("mLock") removeAlarmsLocked(int userId, @NonNull String packageName)525 void removeAlarmsLocked(int userId, @NonNull String packageName) { 526 boolean removed = false; 527 Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); 528 for (int i = alarms.length - 1; i >= 0; --i) { 529 final Uptc uptc = (Uptc) alarms[i].first; 530 if (userId == uptc.userId && packageName.equals(uptc.packageName)) { 531 mAlarmQueue.remove(alarms[i]); 532 removed = true; 533 } 534 } 535 if (removed) { 536 setNextAlarmLocked(); 537 } 538 } 539 540 @GuardedBy("mLock") setNextAlarmLocked()541 private void setNextAlarmLocked() { 542 if (mAlarmQueue.size() > 0) { 543 final long nextTriggerTimeElapsed = mAlarmQueue.peek().second; 544 // Only schedule the alarm if one of the following is true: 545 // 1. There isn't one currently scheduled 546 // 2. The new alarm is significantly earlier than the previous alarm. If it's 547 // earlier but not significantly so, then we essentially delay the notification a 548 // few extra minutes. 549 if (mTriggerTimeElapsed == 0 550 || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS 551 || mTriggerTimeElapsed < nextTriggerTimeElapsed) { 552 // Use a non-wakeup alarm for this 553 scheduleAlarm(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed, 554 ALARM_TAG_QUOTA_CHECK, this); 555 mTriggerTimeElapsed = nextTriggerTimeElapsed; 556 } 557 } else { 558 cancelAlarm(this); 559 mTriggerTimeElapsed = 0; 560 } 561 } 562 563 @Override onAlarm()564 public void onAlarm() { 565 synchronized (mLock) { 566 while (mAlarmQueue.size() > 0) { 567 final Pair<Uptc, Long> alarm = mAlarmQueue.peek(); 568 if (alarm.second <= mInjector.getElapsedRealtime()) { 569 getHandler().post(() -> maybeUpdateQuotaStatus( 570 alarm.first.userId, alarm.first.packageName, alarm.first.tag)); 571 mAlarmQueue.remove(alarm); 572 } else { 573 break; 574 } 575 } 576 setNextAlarmLocked(); 577 } 578 } 579 580 @GuardedBy("mLock") dumpLocked(IndentingPrintWriter pw)581 void dumpLocked(IndentingPrintWriter pw) { 582 pw.println("In quota alarms:"); 583 pw.increaseIndent(); 584 585 if (mAlarmQueue.size() == 0) { 586 pw.println("NOT WAITING"); 587 } else { 588 Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); 589 for (int i = 0; i < alarms.length; ++i) { 590 final Uptc uptc = (Uptc) alarms[i].first; 591 pw.print(uptc); 592 pw.print(": "); 593 pw.print(alarms[i].second); 594 pw.println(); 595 } 596 } 597 598 pw.decreaseIndent(); 599 } 600 601 @GuardedBy("mLock") dumpLocked(ProtoOutputStream proto, long fieldId)602 void dumpLocked(ProtoOutputStream proto, long fieldId) { 603 final long token = proto.start(fieldId); 604 605 proto.write(QuotaTrackerProto.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED, 606 mTriggerTimeElapsed); 607 608 Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]); 609 for (int i = 0; i < alarms.length; ++i) { 610 final long aToken = proto.start(QuotaTrackerProto.InQuotaAlarmListener.ALARMS); 611 612 final Uptc uptc = (Uptc) alarms[i].first; 613 uptc.dumpDebug(proto, QuotaTrackerProto.InQuotaAlarmListener.Alarm.UPTC); 614 proto.write(QuotaTrackerProto.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED, 615 (Long) alarms[i].second); 616 617 proto.end(aToken); 618 } 619 620 proto.end(token); 621 } 622 } 623 624 //////////////////////////// DATA DUMP ////////////////////////////// 625 626 /** Dump state in text format. */ dump(final IndentingPrintWriter pw)627 public void dump(final IndentingPrintWriter pw) { 628 pw.println("QuotaTracker:"); 629 pw.increaseIndent(); 630 631 synchronized (mLock) { 632 pw.println("Is enabled: " + mIsEnabled); 633 pw.println("Is global quota free: " + mIsQuotaFree); 634 pw.println("Current elapsed time: " + mInjector.getElapsedRealtime()); 635 pw.println(); 636 637 pw.println(); 638 mInQuotaAlarmListener.dumpLocked(pw); 639 640 pw.println(); 641 pw.println("Per-app free quota:"); 642 pw.increaseIndent(); 643 for (int u = 0; u < mFreeQuota.numMaps(); ++u) { 644 final int userId = mFreeQuota.keyAt(u); 645 for (int p = 0; p < mFreeQuota.numElementsForKey(userId); ++p) { 646 final String pkgName = mFreeQuota.keyAt(u, p); 647 648 pw.print(string(userId, pkgName, null)); 649 pw.print(": "); 650 pw.println(mFreeQuota.get(userId, pkgName)); 651 } 652 } 653 pw.decreaseIndent(); 654 } 655 656 pw.decreaseIndent(); 657 } 658 659 /** 660 * Dump state to proto. 661 * 662 * @param proto The ProtoOutputStream to write to. 663 * @param fieldId The field ID of the {@link QuotaTrackerProto}. 664 */ dump(ProtoOutputStream proto, long fieldId)665 public void dump(ProtoOutputStream proto, long fieldId) { 666 final long token = proto.start(fieldId); 667 668 synchronized (mLock) { 669 proto.write(QuotaTrackerProto.IS_ENABLED, mIsEnabled); 670 proto.write(QuotaTrackerProto.IS_GLOBAL_QUOTA_FREE, mIsQuotaFree); 671 proto.write(QuotaTrackerProto.ELAPSED_REALTIME, mInjector.getElapsedRealtime()); 672 mInQuotaAlarmListener.dumpLocked(proto, QuotaTrackerProto.IN_QUOTA_ALARM_LISTENER); 673 } 674 675 proto.end(token); 676 } 677 } 678