AppIdleHistory.java revision a93542f9d341897f3206f775fd5720663b17504f
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 17package com.android.server.usage; 18 19import android.os.Environment; 20import android.os.SystemClock; 21import android.os.UserHandle; 22import android.util.ArrayMap; 23import android.util.AtomicFile; 24import android.util.Slog; 25import android.util.SparseArray; 26import android.util.TimeUtils; 27import android.util.Xml; 28 29import com.android.internal.annotations.VisibleForTesting; 30import com.android.internal.util.FastXmlSerializer; 31import com.android.internal.util.IndentingPrintWriter; 32 33import libcore.io.IoUtils; 34 35import org.xmlpull.v1.XmlPullParser; 36import org.xmlpull.v1.XmlPullParserException; 37 38import java.io.BufferedOutputStream; 39import java.io.BufferedReader; 40import java.io.File; 41import java.io.FileInputStream; 42import java.io.FileOutputStream; 43import java.io.FileReader; 44import java.io.IOException; 45import java.nio.charset.StandardCharsets; 46 47/** 48 * Keeps track of recent active state changes in apps. 49 * Access should be guarded by a lock by the caller. 50 */ 51public class AppIdleHistory { 52 53 private static final String TAG = "AppIdleHistory"; 54 55 // History for all users and all packages 56 private SparseArray<ArrayMap<String,PackageHistory>> mIdleHistory = new SparseArray<>(); 57 private long mLastPeriod = 0; 58 private static final long ONE_MINUTE = 60 * 1000; 59 private static final int HISTORY_SIZE = 100; 60 private static final int FLAG_LAST_STATE = 2; 61 private static final int FLAG_PARTIAL_ACTIVE = 1; 62 private static final long PERIOD_DURATION = UsageStatsService.COMPRESS_TIME ? ONE_MINUTE 63 : 60 * ONE_MINUTE; 64 65 @VisibleForTesting 66 static final String APP_IDLE_FILENAME = "app_idle_stats.xml"; 67 private static final String TAG_PACKAGES = "packages"; 68 private static final String TAG_PACKAGE = "package"; 69 private static final String ATTR_NAME = "name"; 70 // Screen on timebase time when app was last used 71 private static final String ATTR_SCREEN_IDLE = "screenIdleTime"; 72 // Elapsed timebase time when app was last used 73 private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime"; 74 75 // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot) 76 private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration 77 private long mElapsedDuration; // Total device on duration since device was "born" 78 79 // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot) 80 private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration 81 private long mScreenOnDuration; // Total screen on duration since device was "born" 82 83 private long mElapsedTimeThreshold; 84 private long mScreenOnTimeThreshold; 85 private final File mStorageDir; 86 87 private boolean mScreenOn; 88 89 private static class PackageHistory { 90 final byte[] recent = new byte[HISTORY_SIZE]; 91 long lastUsedElapsedTime; 92 long lastUsedScreenTime; 93 } 94 95 AppIdleHistory(long elapsedRealtime) { 96 this(Environment.getDataSystemDirectory(), elapsedRealtime); 97 } 98 99 @VisibleForTesting 100 AppIdleHistory(File storageDir, long elapsedRealtime) { 101 mElapsedSnapshot = elapsedRealtime; 102 mScreenOnSnapshot = elapsedRealtime; 103 mStorageDir = storageDir; 104 readScreenOnTimeLocked(); 105 } 106 107 public void setThresholds(long elapsedTimeThreshold, long screenOnTimeThreshold) { 108 mElapsedTimeThreshold = elapsedTimeThreshold; 109 mScreenOnTimeThreshold = screenOnTimeThreshold; 110 } 111 112 public void updateDisplayLocked(boolean screenOn, long elapsedRealtime) { 113 if (screenOn == mScreenOn) return; 114 115 mScreenOn = screenOn; 116 if (mScreenOn) { 117 mScreenOnSnapshot = elapsedRealtime; 118 } else { 119 mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot; 120 mElapsedDuration += elapsedRealtime - mElapsedSnapshot; 121 writeScreenOnTimeLocked(); 122 mElapsedSnapshot = elapsedRealtime; 123 } 124 } 125 126 public long getScreenOnTimeLocked(long elapsedRealtime) { 127 long screenOnTime = mScreenOnDuration; 128 if (mScreenOn) { 129 screenOnTime += elapsedRealtime - mScreenOnSnapshot; 130 } 131 return screenOnTime; 132 } 133 134 @VisibleForTesting 135 File getScreenOnTimeFile() { 136 return new File(mStorageDir, "screen_on_time"); 137 } 138 139 private void readScreenOnTimeLocked() { 140 File screenOnTimeFile = getScreenOnTimeFile(); 141 if (screenOnTimeFile.exists()) { 142 try { 143 BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile)); 144 mScreenOnDuration = Long.parseLong(reader.readLine()); 145 mElapsedDuration = Long.parseLong(reader.readLine()); 146 reader.close(); 147 } catch (IOException | NumberFormatException e) { 148 } 149 } else { 150 writeScreenOnTimeLocked(); 151 } 152 } 153 154 private void writeScreenOnTimeLocked() { 155 AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile()); 156 FileOutputStream fos = null; 157 try { 158 fos = screenOnTimeFile.startWrite(); 159 fos.write((Long.toString(mScreenOnDuration) + "\n" 160 + Long.toString(mElapsedDuration) + "\n").getBytes()); 161 screenOnTimeFile.finishWrite(fos); 162 } catch (IOException ioe) { 163 screenOnTimeFile.failWrite(fos); 164 } 165 } 166 167 /** 168 * To be called periodically to keep track of elapsed time when app idle times are written 169 */ 170 public void writeElapsedTimeLocked() { 171 final long elapsedRealtime = SystemClock.elapsedRealtime(); 172 // Only bump up and snapshot the elapsed time. Don't change screen on duration. 173 mElapsedDuration += elapsedRealtime - mElapsedSnapshot; 174 mElapsedSnapshot = elapsedRealtime; 175 writeScreenOnTimeLocked(); 176 } 177 178 public void reportUsageLocked(String packageName, int userId, long elapsedRealtime) { 179 ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId); 180 PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName, 181 elapsedRealtime); 182 183 shiftHistoryToNow(userHistory, elapsedRealtime); 184 185 packageHistory.lastUsedElapsedTime = mElapsedDuration 186 + (elapsedRealtime - mElapsedSnapshot); 187 packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime); 188 packageHistory.recent[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE; 189 } 190 191 public void setIdle(String packageName, int userId, long elapsedRealtime) { 192 ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId); 193 PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName, 194 elapsedRealtime); 195 196 shiftHistoryToNow(userHistory, elapsedRealtime); 197 198 packageHistory.recent[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE; 199 } 200 201 private void shiftHistoryToNow(ArrayMap<String, PackageHistory> userHistory, 202 long elapsedRealtime) { 203 long thisPeriod = elapsedRealtime / PERIOD_DURATION; 204 // Has the period switched over? Slide all users' package histories 205 if (mLastPeriod != 0 && mLastPeriod < thisPeriod 206 && (thisPeriod - mLastPeriod) < HISTORY_SIZE - 1) { 207 int diff = (int) (thisPeriod - mLastPeriod); 208 final int NUSERS = mIdleHistory.size(); 209 for (int u = 0; u < NUSERS; u++) { 210 userHistory = mIdleHistory.valueAt(u); 211 for (PackageHistory idleState : userHistory.values()) { 212 // Shift left 213 System.arraycopy(idleState.recent, diff, idleState.recent, 0, 214 HISTORY_SIZE - diff); 215 // Replicate last state across the diff 216 for (int i = 0; i < diff; i++) { 217 idleState.recent[HISTORY_SIZE - i - 1] = 218 (byte) (idleState.recent[HISTORY_SIZE - diff - 1] & FLAG_LAST_STATE); 219 } 220 } 221 } 222 } 223 mLastPeriod = thisPeriod; 224 } 225 226 private ArrayMap<String, PackageHistory> getUserHistoryLocked(int userId) { 227 ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId); 228 if (userHistory == null) { 229 userHistory = new ArrayMap<>(); 230 mIdleHistory.put(userId, userHistory); 231 readAppIdleTimesLocked(userId, userHistory); 232 } 233 return userHistory; 234 } 235 236 private PackageHistory getPackageHistoryLocked(ArrayMap<String, PackageHistory> userHistory, 237 String packageName, long elapsedRealtime) { 238 PackageHistory packageHistory = userHistory.get(packageName); 239 if (packageHistory == null) { 240 packageHistory = new PackageHistory(); 241 packageHistory.lastUsedElapsedTime = getElapsedTimeLocked(elapsedRealtime); 242 packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime); 243 userHistory.put(packageName, packageHistory); 244 } 245 return packageHistory; 246 } 247 248 public void onUserRemoved(int userId) { 249 mIdleHistory.remove(userId); 250 } 251 252 public boolean isIdleLocked(String packageName, int userId, long elapsedRealtime) { 253 ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId); 254 PackageHistory packageHistory = 255 getPackageHistoryLocked(userHistory, packageName, elapsedRealtime); 256 if (packageHistory == null) { 257 return false; // Default to not idle 258 } else { 259 return hasPassedThresholdsLocked(packageHistory, elapsedRealtime); 260 } 261 } 262 263 private long getElapsedTimeLocked(long elapsedRealtime) { 264 return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration); 265 } 266 267 public void setIdleLocked(String packageName, int userId, boolean idle, long elapsedRealtime) { 268 ArrayMap<String, PackageHistory> userHistory = getUserHistoryLocked(userId); 269 PackageHistory packageHistory = getPackageHistoryLocked(userHistory, packageName, 270 elapsedRealtime); 271 packageHistory.lastUsedElapsedTime = getElapsedTimeLocked(elapsedRealtime) 272 - mElapsedTimeThreshold; 273 packageHistory.lastUsedScreenTime = getScreenOnTimeLocked(elapsedRealtime) 274 - (idle ? mScreenOnTimeThreshold : 0) - 1000 /* just a second more */; 275 } 276 277 private boolean hasPassedThresholdsLocked(PackageHistory packageHistory, long elapsedRealtime) { 278 return (packageHistory.lastUsedScreenTime 279 <= getScreenOnTimeLocked(elapsedRealtime) - mScreenOnTimeThreshold) 280 && (packageHistory.lastUsedElapsedTime 281 <= getElapsedTimeLocked(elapsedRealtime) - mElapsedTimeThreshold); 282 } 283 284 private File getUserFile(int userId) { 285 return new File(new File(new File(mStorageDir, "users"), 286 Integer.toString(userId)), APP_IDLE_FILENAME); 287 } 288 289 private void readAppIdleTimesLocked(int userId, ArrayMap<String, PackageHistory> userHistory) { 290 FileInputStream fis = null; 291 try { 292 AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); 293 fis = appIdleFile.openRead(); 294 XmlPullParser parser = Xml.newPullParser(); 295 parser.setInput(fis, StandardCharsets.UTF_8.name()); 296 297 int type; 298 while ((type = parser.next()) != XmlPullParser.START_TAG 299 && type != XmlPullParser.END_DOCUMENT) { 300 // Skip 301 } 302 303 if (type != XmlPullParser.START_TAG) { 304 Slog.e(TAG, "Unable to read app idle file for user " + userId); 305 return; 306 } 307 if (!parser.getName().equals(TAG_PACKAGES)) { 308 return; 309 } 310 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 311 if (type == XmlPullParser.START_TAG) { 312 final String name = parser.getName(); 313 if (name.equals(TAG_PACKAGE)) { 314 final String packageName = parser.getAttributeValue(null, ATTR_NAME); 315 PackageHistory packageHistory = new PackageHistory(); 316 packageHistory.lastUsedElapsedTime = 317 Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE)); 318 packageHistory.lastUsedScreenTime = 319 Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE)); 320 userHistory.put(packageName, packageHistory); 321 } 322 } 323 } 324 } catch (IOException | XmlPullParserException e) { 325 Slog.e(TAG, "Unable to read app idle file for user " + userId); 326 } finally { 327 IoUtils.closeQuietly(fis); 328 } 329 } 330 331 public void writeAppIdleTimesLocked(int userId) { 332 FileOutputStream fos = null; 333 AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); 334 try { 335 fos = appIdleFile.startWrite(); 336 final BufferedOutputStream bos = new BufferedOutputStream(fos); 337 338 FastXmlSerializer xml = new FastXmlSerializer(); 339 xml.setOutput(bos, StandardCharsets.UTF_8.name()); 340 xml.startDocument(null, true); 341 xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 342 343 xml.startTag(null, TAG_PACKAGES); 344 345 ArrayMap<String,PackageHistory> userHistory = getUserHistoryLocked(userId); 346 final int N = userHistory.size(); 347 for (int i = 0; i < N; i++) { 348 String packageName = userHistory.keyAt(i); 349 PackageHistory history = userHistory.valueAt(i); 350 xml.startTag(null, TAG_PACKAGE); 351 xml.attribute(null, ATTR_NAME, packageName); 352 xml.attribute(null, ATTR_ELAPSED_IDLE, 353 Long.toString(history.lastUsedElapsedTime)); 354 xml.attribute(null, ATTR_SCREEN_IDLE, 355 Long.toString(history.lastUsedScreenTime)); 356 xml.endTag(null, TAG_PACKAGE); 357 } 358 359 xml.endTag(null, TAG_PACKAGES); 360 xml.endDocument(); 361 appIdleFile.finishWrite(fos); 362 } catch (Exception e) { 363 appIdleFile.failWrite(fos); 364 Slog.e(TAG, "Error writing app idle file for user " + userId); 365 } 366 } 367 368 public void dump(IndentingPrintWriter idpw, int userId) { 369 idpw.println("Package idle stats:"); 370 idpw.increaseIndent(); 371 ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId); 372 final long elapsedRealtime = SystemClock.elapsedRealtime(); 373 final long totalElapsedTime = getElapsedTimeLocked(elapsedRealtime); 374 final long screenOnTime = getScreenOnTimeLocked(elapsedRealtime); 375 if (userHistory == null) return; 376 final int P = userHistory.size(); 377 for (int p = 0; p < P; p++) { 378 final String packageName = userHistory.keyAt(p); 379 final PackageHistory packageHistory = userHistory.valueAt(p); 380 idpw.print("package=" + packageName); 381 idpw.print(" lastUsedElapsed="); 382 TimeUtils.formatDuration(totalElapsedTime - packageHistory.lastUsedElapsedTime, idpw); 383 idpw.print(" lastUsedScreenOn="); 384 TimeUtils.formatDuration(screenOnTime - packageHistory.lastUsedScreenTime, idpw); 385 idpw.print(" idle=" + (isIdleLocked(packageName, userId, elapsedRealtime) ? "y" : "n")); 386 idpw.println(); 387 } 388 idpw.println(); 389 idpw.print("totalElapsedTime="); 390 TimeUtils.formatDuration(getElapsedTimeLocked(elapsedRealtime), idpw); 391 idpw.println(); 392 idpw.print("totalScreenOnTime="); 393 TimeUtils.formatDuration(getScreenOnTimeLocked(elapsedRealtime), idpw); 394 idpw.println(); 395 idpw.decreaseIndent(); 396 } 397 398 public void dumpHistory(IndentingPrintWriter idpw, int userId) { 399 ArrayMap<String, PackageHistory> userHistory = mIdleHistory.get(userId); 400 final long elapsedRealtime = SystemClock.elapsedRealtime(); 401 if (userHistory == null) return; 402 final int P = userHistory.size(); 403 for (int p = 0; p < P; p++) { 404 final String packageName = userHistory.keyAt(p); 405 final byte[] history = userHistory.valueAt(p).recent; 406 for (int i = 0; i < HISTORY_SIZE; i++) { 407 idpw.print(history[i] == 0 ? '.' : 'A'); 408 } 409 idpw.print(" idle=" + (isIdleLocked(packageName, userId, elapsedRealtime) ? "y" : "n")); 410 idpw.print(" " + packageName); 411 idpw.println(); 412 } 413 } 414} 415