1 /** 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations 14 * under the License. 15 */ 16 17 package com.android.server.usage; 18 19 import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT; 20 import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER; 21 import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK; 22 import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED; 23 import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; 24 import static android.app.usage.UsageStatsManager.REASON_SUB_MASK; 25 import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION; 26 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; 27 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; 28 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; 29 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; 30 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; 31 32 import static com.android.server.usage.AppStandbyController.isUserUsage; 33 34 import android.app.usage.AppStandbyInfo; 35 import android.app.usage.UsageStatsManager; 36 import android.os.SystemClock; 37 import android.util.ArrayMap; 38 import android.util.AtomicFile; 39 import android.util.Slog; 40 import android.util.SparseArray; 41 import android.util.TimeUtils; 42 import android.util.Xml; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.internal.util.CollectionUtils; 46 import com.android.internal.util.FastXmlSerializer; 47 import com.android.internal.util.FrameworkStatsLog; 48 import com.android.internal.util.IndentingPrintWriter; 49 50 import libcore.io.IoUtils; 51 52 import org.xmlpull.v1.XmlPullParser; 53 import org.xmlpull.v1.XmlPullParserException; 54 55 import java.io.BufferedOutputStream; 56 import java.io.BufferedReader; 57 import java.io.File; 58 import java.io.FileInputStream; 59 import java.io.FileOutputStream; 60 import java.io.FileReader; 61 import java.io.IOException; 62 import java.nio.charset.StandardCharsets; 63 import java.util.ArrayList; 64 import java.util.List; 65 66 /** 67 * Keeps track of recent active state changes in apps. 68 * Access should be guarded by a lock by the caller. 69 */ 70 public class AppIdleHistory { 71 72 private static final String TAG = "AppIdleHistory"; 73 74 private static final boolean DEBUG = AppStandbyController.DEBUG; 75 76 // History for all users and all packages 77 private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>(); 78 private static final long ONE_MINUTE = 60 * 1000; 79 80 private static final int STANDBY_BUCKET_UNKNOWN = -1; 81 82 /** 83 * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are 84 * considered idle while those in higher buckets are not considered idle. 85 */ 86 static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE; 87 88 @VisibleForTesting 89 static final String APP_IDLE_FILENAME = "app_idle_stats.xml"; 90 private static final String TAG_PACKAGES = "packages"; 91 private static final String TAG_PACKAGE = "package"; 92 private static final String ATTR_NAME = "name"; 93 // Screen on timebase time when app was last used 94 private static final String ATTR_SCREEN_IDLE = "screenIdleTime"; 95 // Elapsed timebase time when app was last used 96 private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime"; 97 // Elapsed timebase time when app was last used by the user 98 private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime"; 99 // Elapsed timebase time when the app bucket was last predicted externally 100 private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime"; 101 // The standby bucket for the app 102 private static final String ATTR_CURRENT_BUCKET = "appLimitBucket"; 103 // The reason the app was put in the above bucket 104 private static final String ATTR_BUCKETING_REASON = "bucketReason"; 105 // The last time a job was run for this app 106 private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime"; 107 // The time when the forced active state can be overridden. 108 private static final String ATTR_BUCKET_ACTIVE_TIMEOUT_TIME = "activeTimeoutTime"; 109 // The time when the forced working_set state can be overridden. 110 private static final String ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME = "workingSetTimeoutTime"; 111 // Elapsed timebase time when the app was last marked for restriction. 112 private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED = 113 "lastRestrictionAttemptElapsedTime"; 114 // Reason why the app was last marked for restriction. 115 private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON = 116 "lastRestrictionAttemptReason"; 117 118 // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot) 119 private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration 120 private long mElapsedDuration; // Total device on duration since device was "born" 121 122 // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot) 123 private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration 124 private long mScreenOnDuration; // Total screen on duration since device was "born" 125 126 private final File mStorageDir; 127 128 private boolean mScreenOn; 129 130 static class AppUsageHistory { 131 // Last used time (including system usage), using elapsed timebase 132 long lastUsedElapsedTime; 133 // Last time the user used the app, using elapsed timebase 134 long lastUsedByUserElapsedTime; 135 // Last used time using screen_on timebase 136 long lastUsedScreenTime; 137 // Last predicted time using elapsed timebase 138 long lastPredictedTime; 139 // Last predicted bucket 140 @UsageStatsManager.StandbyBuckets 141 int lastPredictedBucket = STANDBY_BUCKET_UNKNOWN; 142 // Standby bucket 143 @UsageStatsManager.StandbyBuckets 144 int currentBucket; 145 // Reason for setting the standby bucket. The value here is a combination of 146 // one of UsageStatsManager.REASON_MAIN_* and one (or none) of 147 // UsageStatsManager.REASON_SUB_*. Also see REASON_MAIN_MASK and REASON_SUB_MASK. 148 int bucketingReason; 149 // In-memory only, last bucket for which the listeners were informed 150 int lastInformedBucket; 151 // The last time a job was run for this app, using elapsed timebase 152 long lastJobRunTime; 153 // When should the bucket active state timeout, in elapsed timebase, if greater than 154 // lastUsedElapsedTime. 155 // This is used to keep the app in a high bucket regardless of other timeouts and 156 // predictions. 157 long bucketActiveTimeoutTime; 158 // If there's a forced working_set state, this is when it times out. This can be sitting 159 // under any active state timeout, so that it becomes applicable after the active state 160 // timeout expires. 161 long bucketWorkingSetTimeoutTime; 162 // The last time an agent attempted to put the app into the RESTRICTED bucket. 163 long lastRestrictAttemptElapsedTime; 164 // The last reason the app was marked to be put into the RESTRICTED bucket. 165 int lastRestrictReason; 166 } 167 AppIdleHistory(File storageDir, long elapsedRealtime)168 AppIdleHistory(File storageDir, long elapsedRealtime) { 169 mElapsedSnapshot = elapsedRealtime; 170 mScreenOnSnapshot = elapsedRealtime; 171 mStorageDir = storageDir; 172 readScreenOnTime(); 173 } 174 updateDisplay(boolean screenOn, long elapsedRealtime)175 public void updateDisplay(boolean screenOn, long elapsedRealtime) { 176 if (screenOn == mScreenOn) return; 177 178 mScreenOn = screenOn; 179 if (mScreenOn) { 180 mScreenOnSnapshot = elapsedRealtime; 181 } else { 182 mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot; 183 mElapsedDuration += elapsedRealtime - mElapsedSnapshot; 184 mElapsedSnapshot = elapsedRealtime; 185 } 186 if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot 187 + ", mScreenOnDuration=" + mScreenOnDuration 188 + ", mScreenOn=" + mScreenOn); 189 } 190 getScreenOnTime(long elapsedRealtime)191 public long getScreenOnTime(long elapsedRealtime) { 192 long screenOnTime = mScreenOnDuration; 193 if (mScreenOn) { 194 screenOnTime += elapsedRealtime - mScreenOnSnapshot; 195 } 196 return screenOnTime; 197 } 198 199 @VisibleForTesting getScreenOnTimeFile()200 File getScreenOnTimeFile() { 201 return new File(mStorageDir, "screen_on_time"); 202 } 203 readScreenOnTime()204 private void readScreenOnTime() { 205 File screenOnTimeFile = getScreenOnTimeFile(); 206 if (screenOnTimeFile.exists()) { 207 try { 208 BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile)); 209 mScreenOnDuration = Long.parseLong(reader.readLine()); 210 mElapsedDuration = Long.parseLong(reader.readLine()); 211 reader.close(); 212 } catch (IOException | NumberFormatException e) { 213 } 214 } else { 215 writeScreenOnTime(); 216 } 217 } 218 writeScreenOnTime()219 private void writeScreenOnTime() { 220 AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile()); 221 FileOutputStream fos = null; 222 try { 223 fos = screenOnTimeFile.startWrite(); 224 fos.write((Long.toString(mScreenOnDuration) + "\n" 225 + Long.toString(mElapsedDuration) + "\n").getBytes()); 226 screenOnTimeFile.finishWrite(fos); 227 } catch (IOException ioe) { 228 screenOnTimeFile.failWrite(fos); 229 } 230 } 231 232 /** 233 * To be called periodically to keep track of elapsed time when app idle times are written 234 */ writeAppIdleDurations()235 public void writeAppIdleDurations() { 236 final long elapsedRealtime = SystemClock.elapsedRealtime(); 237 // Only bump up and snapshot the elapsed time. Don't change screen on duration. 238 mElapsedDuration += elapsedRealtime - mElapsedSnapshot; 239 mElapsedSnapshot = elapsedRealtime; 240 writeScreenOnTime(); 241 } 242 243 /** 244 * Mark the app as used and update the bucket if necessary. If there is a timeout specified 245 * that's in the future, then the usage event is temporary and keeps the app in the specified 246 * bucket at least until the timeout is reached. This can be used to keep the app in an 247 * elevated bucket for a while until some important task gets to run. 248 * @param appUsageHistory the usage record for the app being updated 249 * @param packageName name of the app being updated, for logging purposes 250 * @param newBucket the bucket to set the app to 251 * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_* 252 * @param elapsedRealtime mark as used time if non-zero 253 * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used 254 * with bucket values of ACTIVE and WORKING_SET. 255 * @return {@code appUsageHistory} 256 */ reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, int newBucket, int usageReason, long elapsedRealtime, long timeout)257 AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, 258 int newBucket, int usageReason, long elapsedRealtime, long timeout) { 259 int bucketingReason = REASON_MAIN_USAGE | usageReason; 260 final boolean isUserUsage = isUserUsage(bucketingReason); 261 262 if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage) { 263 // Only user usage should bring an app out of the RESTRICTED bucket. 264 newBucket = STANDBY_BUCKET_RESTRICTED; 265 bucketingReason = appUsageHistory.bucketingReason; 266 } else { 267 // Set the timeout if applicable 268 if (timeout > elapsedRealtime) { 269 // Convert to elapsed timebase 270 final long timeoutTime = mElapsedDuration + (timeout - mElapsedSnapshot); 271 if (newBucket == STANDBY_BUCKET_ACTIVE) { 272 appUsageHistory.bucketActiveTimeoutTime = Math.max(timeoutTime, 273 appUsageHistory.bucketActiveTimeoutTime); 274 } else if (newBucket == STANDBY_BUCKET_WORKING_SET) { 275 appUsageHistory.bucketWorkingSetTimeoutTime = Math.max(timeoutTime, 276 appUsageHistory.bucketWorkingSetTimeoutTime); 277 } else { 278 throw new IllegalArgumentException("Cannot set a timeout on bucket=" 279 + newBucket); 280 } 281 } 282 } 283 284 if (elapsedRealtime != 0) { 285 appUsageHistory.lastUsedElapsedTime = mElapsedDuration 286 + (elapsedRealtime - mElapsedSnapshot); 287 if (isUserUsage) { 288 appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime; 289 } 290 appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); 291 } 292 293 if (appUsageHistory.currentBucket > newBucket) { 294 appUsageHistory.currentBucket = newBucket; 295 logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason); 296 } 297 appUsageHistory.bucketingReason = bucketingReason; 298 299 return appUsageHistory; 300 } 301 302 /** 303 * Mark the app as used and update the bucket if necessary. If there is a timeout specified 304 * that's in the future, then the usage event is temporary and keeps the app in the specified 305 * bucket at least until the timeout is reached. This can be used to keep the app in an 306 * elevated bucket for a while until some important task gets to run. 307 * @param packageName 308 * @param userId 309 * @param newBucket the bucket to set the app to 310 * @param usageReason sub reason for usage 311 * @param nowElapsed mark as used time if non-zero 312 * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used 313 * with bucket values of ACTIVE and WORKING_SET. 314 * @return 315 */ reportUsage(String packageName, int userId, int newBucket, int usageReason, long nowElapsed, long timeout)316 public AppUsageHistory reportUsage(String packageName, int userId, int newBucket, 317 int usageReason, long nowElapsed, long timeout) { 318 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 319 AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true); 320 return reportUsage(history, packageName, userId, newBucket, usageReason, nowElapsed, 321 timeout); 322 } 323 getUserHistory(int userId)324 private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) { 325 ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); 326 if (userHistory == null) { 327 userHistory = new ArrayMap<>(); 328 mIdleHistory.put(userId, userHistory); 329 readAppIdleTimes(userId, userHistory); 330 } 331 return userHistory; 332 } 333 getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory, String packageName, long elapsedRealtime, boolean create)334 private AppUsageHistory getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory, 335 String packageName, long elapsedRealtime, boolean create) { 336 AppUsageHistory appUsageHistory = userHistory.get(packageName); 337 if (appUsageHistory == null && create) { 338 appUsageHistory = new AppUsageHistory(); 339 appUsageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime); 340 appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); 341 appUsageHistory.lastPredictedTime = getElapsedTime(0); 342 appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER; 343 appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; 344 appUsageHistory.lastInformedBucket = -1; 345 appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago 346 userHistory.put(packageName, appUsageHistory); 347 } 348 return appUsageHistory; 349 } 350 onUserRemoved(int userId)351 public void onUserRemoved(int userId) { 352 mIdleHistory.remove(userId); 353 } 354 isIdle(String packageName, int userId, long elapsedRealtime)355 public boolean isIdle(String packageName, int userId, long elapsedRealtime) { 356 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 357 AppUsageHistory appUsageHistory = 358 getPackageHistory(userHistory, packageName, elapsedRealtime, true); 359 return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF; 360 } 361 getAppUsageHistory(String packageName, int userId, long elapsedRealtime)362 public AppUsageHistory getAppUsageHistory(String packageName, int userId, 363 long elapsedRealtime) { 364 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 365 AppUsageHistory appUsageHistory = 366 getPackageHistory(userHistory, packageName, elapsedRealtime, true); 367 return appUsageHistory; 368 } 369 setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason)370 public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, 371 int bucket, int reason) { 372 setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false); 373 } 374 setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason, boolean resetTimeout)375 public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, 376 int bucket, int reason, boolean resetTimeout) { 377 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 378 AppUsageHistory appUsageHistory = 379 getPackageHistory(userHistory, packageName, elapsedRealtime, true); 380 final boolean changed = appUsageHistory.currentBucket != bucket; 381 appUsageHistory.currentBucket = bucket; 382 appUsageHistory.bucketingReason = reason; 383 384 final long elapsed = getElapsedTime(elapsedRealtime); 385 386 if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) { 387 appUsageHistory.lastPredictedTime = elapsed; 388 appUsageHistory.lastPredictedBucket = bucket; 389 } 390 if (resetTimeout) { 391 appUsageHistory.bucketActiveTimeoutTime = elapsed; 392 appUsageHistory.bucketWorkingSetTimeoutTime = elapsed; 393 } 394 if (changed) { 395 logAppStandbyBucketChanged(packageName, userId, bucket, reason); 396 } 397 } 398 399 /** 400 * Update the prediction for the app but don't change the actual bucket 401 * @param app The app for which the prediction was made 402 * @param elapsedTimeAdjusted The elapsed time in the elapsed duration timebase 403 * @param bucket The predicted bucket 404 */ updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket)405 public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) { 406 app.lastPredictedTime = elapsedTimeAdjusted; 407 app.lastPredictedBucket = bucket; 408 } 409 410 /** 411 * Marks the last time a job was run, with the given elapsedRealtime. The time stored is 412 * based on the elapsed timebase. 413 * @param packageName 414 * @param userId 415 * @param elapsedRealtime 416 */ setLastJobRunTime(String packageName, int userId, long elapsedRealtime)417 public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) { 418 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 419 AppUsageHistory appUsageHistory = 420 getPackageHistory(userHistory, packageName, elapsedRealtime, true); 421 appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime); 422 } 423 424 /** 425 * Notes an attempt to put the app in the {@link UsageStatsManager#STANDBY_BUCKET_RESTRICTED} 426 * bucket. 427 * 428 * @param packageName The package name of the app that is being restricted 429 * @param userId The ID of the user in which the app is being restricted 430 * @param elapsedRealtime The time the attempt was made, in the (unadjusted) elapsed realtime 431 * timebase 432 * @param reason The reason for the restriction attempt 433 */ noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason)434 void noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason) { 435 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 436 AppUsageHistory appUsageHistory = 437 getPackageHistory(userHistory, packageName, elapsedRealtime, true); 438 appUsageHistory.lastRestrictAttemptElapsedTime = getElapsedTime(elapsedRealtime); 439 appUsageHistory.lastRestrictReason = reason; 440 } 441 442 /** 443 * Returns the time since the last job was run for this app. This can be larger than the 444 * current elapsedRealtime, in case it happened before boot or a really large value if no jobs 445 * were ever run. 446 * @param packageName 447 * @param userId 448 * @param elapsedRealtime 449 * @return 450 */ getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime)451 public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) { 452 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 453 AppUsageHistory appUsageHistory = 454 getPackageHistory(userHistory, packageName, elapsedRealtime, false); 455 // Don't adjust the default, else it'll wrap around to a positive value 456 if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) { 457 return Long.MAX_VALUE; 458 } 459 return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime; 460 } 461 getAppStandbyBucket(String packageName, int userId, long elapsedRealtime)462 public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) { 463 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 464 AppUsageHistory appUsageHistory = 465 getPackageHistory(userHistory, packageName, elapsedRealtime, false); 466 return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket; 467 } 468 getAppStandbyBuckets(int userId, boolean appIdleEnabled)469 public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) { 470 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 471 int size = userHistory.size(); 472 ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size); 473 for (int i = 0; i < size; i++) { 474 buckets.add(new AppStandbyInfo(userHistory.keyAt(i), 475 appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE)); 476 } 477 return buckets; 478 } 479 getAppStandbyReason(String packageName, int userId, long elapsedRealtime)480 public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) { 481 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 482 AppUsageHistory appUsageHistory = 483 getPackageHistory(userHistory, packageName, elapsedRealtime, false); 484 return appUsageHistory != null ? appUsageHistory.bucketingReason : 0; 485 } 486 getElapsedTime(long elapsedRealtime)487 public long getElapsedTime(long elapsedRealtime) { 488 return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration); 489 } 490 491 /* Returns the new standby bucket the app is assigned to */ setIdle(String packageName, int userId, boolean idle, long elapsedRealtime)492 public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) { 493 final int newBucket; 494 final int reason; 495 if (idle) { 496 newBucket = IDLE_BUCKET_CUTOFF; 497 reason = REASON_MAIN_FORCED_BY_USER; 498 } else { 499 newBucket = STANDBY_BUCKET_ACTIVE; 500 // This is to pretend that the app was just used, don't freeze the state anymore. 501 reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION; 502 } 503 setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, false); 504 505 return newBucket; 506 } 507 clearUsage(String packageName, int userId)508 public void clearUsage(String packageName, int userId) { 509 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 510 userHistory.remove(packageName); 511 } 512 shouldInformListeners(String packageName, int userId, long elapsedRealtime, int bucket)513 boolean shouldInformListeners(String packageName, int userId, 514 long elapsedRealtime, int bucket) { 515 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 516 AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, 517 elapsedRealtime, true); 518 if (appUsageHistory.lastInformedBucket != bucket) { 519 appUsageHistory.lastInformedBucket = bucket; 520 return true; 521 } 522 return false; 523 } 524 525 /** 526 * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds 527 * that corresponds to how long since the app was used. 528 * @param packageName 529 * @param userId 530 * @param elapsedRealtime current time 531 * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0 532 * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0 533 * @return The index whose values the app's used time exceeds (in both arrays) 534 */ getThresholdIndex(String packageName, int userId, long elapsedRealtime, long[] screenTimeThresholds, long[] elapsedTimeThresholds)535 int getThresholdIndex(String packageName, int userId, long elapsedRealtime, 536 long[] screenTimeThresholds, long[] elapsedTimeThresholds) { 537 ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); 538 AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, 539 elapsedRealtime, false); 540 // If we don't have any state for the app, assume never used 541 if (appUsageHistory == null) return screenTimeThresholds.length - 1; 542 543 long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime; 544 long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime; 545 546 if (DEBUG) Slog.d(TAG, packageName 547 + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime 548 + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime); 549 if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta 550 + ", elapsed=" + elapsedDelta); 551 for (int i = screenTimeThresholds.length - 1; i >= 0; i--) { 552 if (screenOnDelta >= screenTimeThresholds[i] 553 && elapsedDelta >= elapsedTimeThresholds[i]) { 554 return i; 555 } 556 } 557 return 0; 558 } 559 560 /** 561 * Log a standby bucket change to statsd, and also logcat if debug logging is enabled. 562 */ logAppStandbyBucketChanged(String packageName, int userId, int bucket, int reason)563 private void logAppStandbyBucketChanged(String packageName, int userId, int bucket, 564 int reason) { 565 FrameworkStatsLog.write( 566 FrameworkStatsLog.APP_STANDBY_BUCKET_CHANGED, 567 packageName, userId, bucket, 568 (reason & REASON_MAIN_MASK), (reason & REASON_SUB_MASK)); 569 if (DEBUG) { 570 Slog.d(TAG, "Moved " + packageName + " to bucket=" + bucket 571 + ", reason=0x0" + Integer.toHexString(reason)); 572 } 573 } 574 575 @VisibleForTesting getUserFile(int userId)576 File getUserFile(int userId) { 577 return new File(new File(new File(mStorageDir, "users"), 578 Integer.toString(userId)), APP_IDLE_FILENAME); 579 } 580 581 /** 582 * Check if App Idle File exists on disk 583 * @param userId 584 * @return true if file exists 585 */ userFileExists(int userId)586 public boolean userFileExists(int userId) { 587 return getUserFile(userId).exists(); 588 } 589 readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory)590 private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) { 591 FileInputStream fis = null; 592 try { 593 AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); 594 fis = appIdleFile.openRead(); 595 XmlPullParser parser = Xml.newPullParser(); 596 parser.setInput(fis, StandardCharsets.UTF_8.name()); 597 598 int type; 599 while ((type = parser.next()) != XmlPullParser.START_TAG 600 && type != XmlPullParser.END_DOCUMENT) { 601 // Skip 602 } 603 604 if (type != XmlPullParser.START_TAG) { 605 Slog.e(TAG, "Unable to read app idle file for user " + userId); 606 return; 607 } 608 if (!parser.getName().equals(TAG_PACKAGES)) { 609 return; 610 } 611 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 612 if (type == XmlPullParser.START_TAG) { 613 final String name = parser.getName(); 614 if (name.equals(TAG_PACKAGE)) { 615 final String packageName = parser.getAttributeValue(null, ATTR_NAME); 616 AppUsageHistory appUsageHistory = new AppUsageHistory(); 617 appUsageHistory.lastUsedElapsedTime = 618 Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE)); 619 appUsageHistory.lastUsedByUserElapsedTime = getLongValue(parser, 620 ATTR_LAST_USED_BY_USER_ELAPSED, 621 appUsageHistory.lastUsedElapsedTime); 622 appUsageHistory.lastUsedScreenTime = 623 Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE)); 624 appUsageHistory.lastPredictedTime = getLongValue(parser, 625 ATTR_LAST_PREDICTED_TIME, 0L); 626 String currentBucketString = parser.getAttributeValue(null, 627 ATTR_CURRENT_BUCKET); 628 appUsageHistory.currentBucket = currentBucketString == null 629 ? STANDBY_BUCKET_ACTIVE 630 : Integer.parseInt(currentBucketString); 631 String bucketingReason = 632 parser.getAttributeValue(null, ATTR_BUCKETING_REASON); 633 appUsageHistory.lastJobRunTime = getLongValue(parser, 634 ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE); 635 appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser, 636 ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L); 637 appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser, 638 ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L); 639 appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; 640 if (bucketingReason != null) { 641 try { 642 appUsageHistory.bucketingReason = 643 Integer.parseInt(bucketingReason, 16); 644 } catch (NumberFormatException nfe) { 645 Slog.wtf(TAG, "Unable to read bucketing reason", nfe); 646 } 647 } 648 appUsageHistory.lastRestrictAttemptElapsedTime = 649 getLongValue(parser, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 0); 650 String lastRestrictReason = parser.getAttributeValue( 651 null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON); 652 if (lastRestrictReason != null) { 653 try { 654 appUsageHistory.lastRestrictReason = 655 Integer.parseInt(lastRestrictReason, 16); 656 } catch (NumberFormatException nfe) { 657 Slog.wtf(TAG, "Unable to read last restrict reason", nfe); 658 } 659 } 660 appUsageHistory.lastInformedBucket = -1; 661 userHistory.put(packageName, appUsageHistory); 662 } 663 } 664 } 665 } catch (IOException | XmlPullParserException e) { 666 Slog.e(TAG, "Unable to read app idle file for user " + userId, e); 667 } finally { 668 IoUtils.closeQuietly(fis); 669 } 670 } 671 getLongValue(XmlPullParser parser, String attrName, long defValue)672 private long getLongValue(XmlPullParser parser, String attrName, long defValue) { 673 String value = parser.getAttributeValue(null, attrName); 674 if (value == null) return defValue; 675 return Long.parseLong(value); 676 } 677 678 writeAppIdleTimes()679 public void writeAppIdleTimes() { 680 final int size = mIdleHistory.size(); 681 for (int i = 0; i < size; i++) { 682 writeAppIdleTimes(mIdleHistory.keyAt(i)); 683 } 684 } 685 writeAppIdleTimes(int userId)686 public void writeAppIdleTimes(int userId) { 687 FileOutputStream fos = null; 688 AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); 689 try { 690 fos = appIdleFile.startWrite(); 691 final BufferedOutputStream bos = new BufferedOutputStream(fos); 692 693 FastXmlSerializer xml = new FastXmlSerializer(); 694 xml.setOutput(bos, StandardCharsets.UTF_8.name()); 695 xml.startDocument(null, true); 696 xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 697 698 xml.startTag(null, TAG_PACKAGES); 699 700 ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId); 701 final int N = userHistory.size(); 702 for (int i = 0; i < N; i++) { 703 String packageName = userHistory.keyAt(i); 704 // Skip any unexpected null package names 705 if (packageName == null) { 706 Slog.w(TAG, "Skipping App Idle write for unexpected null package"); 707 continue; 708 } 709 AppUsageHistory history = userHistory.valueAt(i); 710 xml.startTag(null, TAG_PACKAGE); 711 xml.attribute(null, ATTR_NAME, packageName); 712 xml.attribute(null, ATTR_ELAPSED_IDLE, 713 Long.toString(history.lastUsedElapsedTime)); 714 xml.attribute(null, ATTR_LAST_USED_BY_USER_ELAPSED, 715 Long.toString(history.lastUsedByUserElapsedTime)); 716 xml.attribute(null, ATTR_SCREEN_IDLE, 717 Long.toString(history.lastUsedScreenTime)); 718 xml.attribute(null, ATTR_LAST_PREDICTED_TIME, 719 Long.toString(history.lastPredictedTime)); 720 xml.attribute(null, ATTR_CURRENT_BUCKET, 721 Integer.toString(history.currentBucket)); 722 xml.attribute(null, ATTR_BUCKETING_REASON, 723 Integer.toHexString(history.bucketingReason)); 724 if (history.bucketActiveTimeoutTime > 0) { 725 xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history 726 .bucketActiveTimeoutTime)); 727 } 728 if (history.bucketWorkingSetTimeoutTime > 0) { 729 xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history 730 .bucketWorkingSetTimeoutTime)); 731 } 732 if (history.lastJobRunTime != Long.MIN_VALUE) { 733 xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history 734 .lastJobRunTime)); 735 } 736 if (history.lastRestrictAttemptElapsedTime > 0) { 737 xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 738 Long.toString(history.lastRestrictAttemptElapsedTime)); 739 } 740 xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON, 741 Integer.toHexString(history.lastRestrictReason)); 742 xml.endTag(null, TAG_PACKAGE); 743 } 744 745 xml.endTag(null, TAG_PACKAGES); 746 xml.endDocument(); 747 appIdleFile.finishWrite(fos); 748 } catch (Exception e) { 749 appIdleFile.failWrite(fos); 750 Slog.e(TAG, "Error writing app idle file for user " + userId, e); 751 } 752 } 753 dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs)754 public void dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) { 755 final int numUsers = userIds.length; 756 for (int i = 0; i < numUsers; i++) { 757 idpw.println(); 758 dumpUser(idpw, userIds[i], pkgs); 759 } 760 } 761 dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs)762 private void dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs) { 763 idpw.print("User "); 764 idpw.print(userId); 765 idpw.println(" App Standby States:"); 766 idpw.increaseIndent(); 767 ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); 768 final long elapsedRealtime = SystemClock.elapsedRealtime(); 769 final long totalElapsedTime = getElapsedTime(elapsedRealtime); 770 final long screenOnTime = getScreenOnTime(elapsedRealtime); 771 if (userHistory == null) return; 772 final int P = userHistory.size(); 773 for (int p = 0; p < P; p++) { 774 final String packageName = userHistory.keyAt(p); 775 final AppUsageHistory appUsageHistory = userHistory.valueAt(p); 776 if (!CollectionUtils.isEmpty(pkgs) && !pkgs.contains(packageName)) { 777 continue; 778 } 779 idpw.print("package=" + packageName); 780 idpw.print(" u=" + userId); 781 idpw.print(" bucket=" + appUsageHistory.currentBucket 782 + " reason=" 783 + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason)); 784 idpw.print(" used="); 785 TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw); 786 idpw.print(" usedByUser="); 787 TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedByUserElapsedTime, 788 idpw); 789 idpw.print(" usedScr="); 790 TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw); 791 idpw.print(" lastPred="); 792 TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw); 793 idpw.print(" activeLeft="); 794 TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime, 795 idpw); 796 idpw.print(" wsLeft="); 797 TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime, 798 idpw); 799 idpw.print(" lastJob="); 800 TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw); 801 if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) { 802 idpw.print(" lastRestrictAttempt="); 803 TimeUtils.formatDuration( 804 totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw); 805 idpw.print(" lastRestrictReason=" 806 + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason)); 807 } 808 idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n")); 809 idpw.println(); 810 } 811 idpw.println(); 812 idpw.print("totalElapsedTime="); 813 TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw); 814 idpw.println(); 815 idpw.print("totalScreenOnTime="); 816 TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw); 817 idpw.println(); 818 idpw.decreaseIndent(); 819 } 820 } 821