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