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