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