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