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         return appUsageHistory.currentBucket >= STANDBY_BUCKET_RARE;
324     }
325 
getAppUsageHistory(String packageName, int userId, long elapsedRealtime)326     public AppUsageHistory getAppUsageHistory(String packageName, int userId,
327             long elapsedRealtime) {
328         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
329         AppUsageHistory appUsageHistory =
330                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
331         return appUsageHistory;
332     }
333 
setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason)334     public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
335             int bucket, int reason) {
336         setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false);
337     }
338 
setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason, boolean resetTimeout)339     public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
340             int bucket, int reason, boolean resetTimeout) {
341         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
342         AppUsageHistory appUsageHistory =
343                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
344         appUsageHistory.currentBucket = bucket;
345         appUsageHistory.bucketingReason = reason;
346 
347         final long elapsed = getElapsedTime(elapsedRealtime);
348 
349         if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) {
350             appUsageHistory.lastPredictedTime = elapsed;
351             appUsageHistory.lastPredictedBucket = bucket;
352         }
353         if (resetTimeout) {
354             appUsageHistory.bucketActiveTimeoutTime = elapsed;
355             appUsageHistory.bucketWorkingSetTimeoutTime = elapsed;
356         }
357         if (DEBUG) {
358             Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket
359                     + ", reason=0x0" + Integer.toHexString(appUsageHistory.bucketingReason));
360         }
361     }
362 
363     /**
364      * Update the prediction for the app but don't change the actual bucket
365      * @param app The app for which the prediction was made
366      * @param elapsedTimeAdjusted The elapsed time in the elapsed duration timebase
367      * @param bucket The predicted bucket
368      */
updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket)369     public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) {
370         app.lastPredictedTime = elapsedTimeAdjusted;
371         app.lastPredictedBucket = bucket;
372     }
373 
374     /**
375      * Marks the last time a job was run, with the given elapsedRealtime. The time stored is
376      * based on the elapsed timebase.
377      * @param packageName
378      * @param userId
379      * @param elapsedRealtime
380      */
setLastJobRunTime(String packageName, int userId, long elapsedRealtime)381     public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
382         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
383         AppUsageHistory appUsageHistory =
384                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
385         appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime);
386     }
387 
388     /**
389      * Returns the time since the last job was run for this app. This can be larger than the
390      * current elapsedRealtime, in case it happened before boot or a really large value if no jobs
391      * were ever run.
392      * @param packageName
393      * @param userId
394      * @param elapsedRealtime
395      * @return
396      */
getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime)397     public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) {
398         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
399         AppUsageHistory appUsageHistory =
400                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
401         // Don't adjust the default, else it'll wrap around to a positive value
402         if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) {
403             return Long.MAX_VALUE;
404         }
405         return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime;
406     }
407 
getAppStandbyBucket(String packageName, int userId, long elapsedRealtime)408     public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) {
409         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
410         AppUsageHistory appUsageHistory =
411                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
412         return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket;
413     }
414 
getAppStandbyBuckets(int userId, boolean appIdleEnabled)415     public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) {
416         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
417         int size = userHistory.size();
418         ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size);
419         for (int i = 0; i < size; i++) {
420             buckets.add(new AppStandbyInfo(userHistory.keyAt(i),
421                     appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE));
422         }
423         return buckets;
424     }
425 
getAppStandbyReason(String packageName, int userId, long elapsedRealtime)426     public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) {
427         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
428         AppUsageHistory appUsageHistory =
429                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
430         return appUsageHistory != null ? appUsageHistory.bucketingReason : 0;
431     }
432 
getElapsedTime(long elapsedRealtime)433     public long getElapsedTime(long elapsedRealtime) {
434         return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
435     }
436 
437     /* Returns the new standby bucket the app is assigned to */
setIdle(String packageName, int userId, boolean idle, long elapsedRealtime)438     public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
439         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
440         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
441                 elapsedRealtime, true);
442         if (idle) {
443             appUsageHistory.currentBucket = STANDBY_BUCKET_RARE;
444             appUsageHistory.bucketingReason = REASON_MAIN_FORCED;
445         } else {
446             appUsageHistory.currentBucket = STANDBY_BUCKET_ACTIVE;
447             // This is to pretend that the app was just used, don't freeze the state anymore.
448             appUsageHistory.bucketingReason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION;
449         }
450         return appUsageHistory.currentBucket;
451     }
452 
clearUsage(String packageName, int userId)453     public void clearUsage(String packageName, int userId) {
454         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
455         userHistory.remove(packageName);
456     }
457 
shouldInformListeners(String packageName, int userId, long elapsedRealtime, int bucket)458     boolean shouldInformListeners(String packageName, int userId,
459             long elapsedRealtime, int bucket) {
460         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
461         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
462                 elapsedRealtime, true);
463         if (appUsageHistory.lastInformedBucket != bucket) {
464             appUsageHistory.lastInformedBucket = bucket;
465             return true;
466         }
467         return false;
468     }
469 
470     /**
471      * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds
472      * that corresponds to how long since the app was used.
473      * @param packageName
474      * @param userId
475      * @param elapsedRealtime current time
476      * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0
477      * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0
478      * @return The index whose values the app's used time exceeds (in both arrays)
479      */
getThresholdIndex(String packageName, int userId, long elapsedRealtime, long[] screenTimeThresholds, long[] elapsedTimeThresholds)480     int getThresholdIndex(String packageName, int userId, long elapsedRealtime,
481             long[] screenTimeThresholds, long[] elapsedTimeThresholds) {
482         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
483         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
484                 elapsedRealtime, false);
485         // If we don't have any state for the app, assume never used
486         if (appUsageHistory == null) return screenTimeThresholds.length - 1;
487 
488         long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
489         long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
490 
491         if (DEBUG) Slog.d(TAG, packageName
492                 + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime
493                 + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime);
494         if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta
495                 + ", elapsed=" + elapsedDelta);
496         for (int i = screenTimeThresholds.length - 1; i >= 0; i--) {
497             if (screenOnDelta >= screenTimeThresholds[i]
498                 && elapsedDelta >= elapsedTimeThresholds[i]) {
499                 return i;
500             }
501         }
502         return 0;
503     }
504 
505     @VisibleForTesting
getUserFile(int userId)506     File getUserFile(int userId) {
507         return new File(new File(new File(mStorageDir, "users"),
508                 Integer.toString(userId)), APP_IDLE_FILENAME);
509     }
510 
511 
512     /**
513      * Check if App Idle File exists on disk
514      * @param userId
515      * @return true if file exists
516      */
userFileExists(int userId)517     public boolean userFileExists(int userId) {
518         return getUserFile(userId).exists();
519     }
520 
readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory)521     private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) {
522         FileInputStream fis = null;
523         try {
524             AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
525             fis = appIdleFile.openRead();
526             XmlPullParser parser = Xml.newPullParser();
527             parser.setInput(fis, StandardCharsets.UTF_8.name());
528 
529             int type;
530             while ((type = parser.next()) != XmlPullParser.START_TAG
531                     && type != XmlPullParser.END_DOCUMENT) {
532                 // Skip
533             }
534 
535             if (type != XmlPullParser.START_TAG) {
536                 Slog.e(TAG, "Unable to read app idle file for user " + userId);
537                 return;
538             }
539             if (!parser.getName().equals(TAG_PACKAGES)) {
540                 return;
541             }
542             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
543                 if (type == XmlPullParser.START_TAG) {
544                     final String name = parser.getName();
545                     if (name.equals(TAG_PACKAGE)) {
546                         final String packageName = parser.getAttributeValue(null, ATTR_NAME);
547                         AppUsageHistory appUsageHistory = new AppUsageHistory();
548                         appUsageHistory.lastUsedElapsedTime =
549                                 Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
550                         appUsageHistory.lastUsedScreenTime =
551                                 Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
552                         appUsageHistory.lastPredictedTime = getLongValue(parser,
553                                 ATTR_LAST_PREDICTED_TIME, 0L);
554                         String currentBucketString = parser.getAttributeValue(null,
555                                 ATTR_CURRENT_BUCKET);
556                         appUsageHistory.currentBucket = currentBucketString == null
557                                 ? STANDBY_BUCKET_ACTIVE
558                                 : Integer.parseInt(currentBucketString);
559                         String bucketingReason =
560                                 parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
561                         appUsageHistory.lastJobRunTime = getLongValue(parser,
562                                 ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE);
563                         appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser,
564                                 ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L);
565                         appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser,
566                                 ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L);
567                         appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT;
568                         if (bucketingReason != null) {
569                             try {
570                                 appUsageHistory.bucketingReason =
571                                         Integer.parseInt(bucketingReason, 16);
572                             } catch (NumberFormatException nfe) {
573                             }
574                         }
575                         appUsageHistory.lastInformedBucket = -1;
576                         userHistory.put(packageName, appUsageHistory);
577                     }
578                 }
579             }
580         } catch (IOException | XmlPullParserException e) {
581             Slog.e(TAG, "Unable to read app idle file for user " + userId, e);
582         } finally {
583             IoUtils.closeQuietly(fis);
584         }
585     }
586 
getLongValue(XmlPullParser parser, String attrName, long defValue)587     private long getLongValue(XmlPullParser parser, String attrName, long defValue) {
588         String value = parser.getAttributeValue(null, attrName);
589         if (value == null) return defValue;
590         return Long.parseLong(value);
591     }
592 
writeAppIdleTimes(int userId)593     public void writeAppIdleTimes(int userId) {
594         FileOutputStream fos = null;
595         AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
596         try {
597             fos = appIdleFile.startWrite();
598             final BufferedOutputStream bos = new BufferedOutputStream(fos);
599 
600             FastXmlSerializer xml = new FastXmlSerializer();
601             xml.setOutput(bos, StandardCharsets.UTF_8.name());
602             xml.startDocument(null, true);
603             xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
604 
605             xml.startTag(null, TAG_PACKAGES);
606 
607             ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId);
608             final int N = userHistory.size();
609             for (int i = 0; i < N; i++) {
610                 String packageName = userHistory.keyAt(i);
611                 // Skip any unexpected null package names
612                 if (packageName == null) {
613                     Slog.w(TAG, "Skipping App Idle write for unexpected null package");
614                     continue;
615                 }
616                 AppUsageHistory history = userHistory.valueAt(i);
617                 xml.startTag(null, TAG_PACKAGE);
618                 xml.attribute(null, ATTR_NAME, packageName);
619                 xml.attribute(null, ATTR_ELAPSED_IDLE,
620                         Long.toString(history.lastUsedElapsedTime));
621                 xml.attribute(null, ATTR_SCREEN_IDLE,
622                         Long.toString(history.lastUsedScreenTime));
623                 xml.attribute(null, ATTR_LAST_PREDICTED_TIME,
624                         Long.toString(history.lastPredictedTime));
625                 xml.attribute(null, ATTR_CURRENT_BUCKET,
626                         Integer.toString(history.currentBucket));
627                 xml.attribute(null, ATTR_BUCKETING_REASON,
628                         Integer.toHexString(history.bucketingReason));
629                 if (history.bucketActiveTimeoutTime > 0) {
630                     xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history
631                             .bucketActiveTimeoutTime));
632                 }
633                 if (history.bucketWorkingSetTimeoutTime > 0) {
634                     xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history
635                             .bucketWorkingSetTimeoutTime));
636                 }
637                 if (history.lastJobRunTime != Long.MIN_VALUE) {
638                     xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history
639                             .lastJobRunTime));
640                 }
641                 xml.endTag(null, TAG_PACKAGE);
642             }
643 
644             xml.endTag(null, TAG_PACKAGES);
645             xml.endDocument();
646             appIdleFile.finishWrite(fos);
647         } catch (Exception e) {
648             appIdleFile.failWrite(fos);
649             Slog.e(TAG, "Error writing app idle file for user " + userId, e);
650         }
651     }
652 
dump(IndentingPrintWriter idpw, int userId, String pkg)653     public void dump(IndentingPrintWriter idpw, int userId, String pkg) {
654         idpw.println("App Standby States:");
655         idpw.increaseIndent();
656         ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
657         final long elapsedRealtime = SystemClock.elapsedRealtime();
658         final long totalElapsedTime = getElapsedTime(elapsedRealtime);
659         final long screenOnTime = getScreenOnTime(elapsedRealtime);
660         if (userHistory == null) return;
661         final int P = userHistory.size();
662         for (int p = 0; p < P; p++) {
663             final String packageName = userHistory.keyAt(p);
664             final AppUsageHistory appUsageHistory = userHistory.valueAt(p);
665             if (pkg != null && !pkg.equals(packageName)) {
666                 continue;
667             }
668             idpw.print("package=" + packageName);
669             idpw.print(" u=" + userId);
670             idpw.print(" bucket=" + appUsageHistory.currentBucket
671                     + " reason="
672                     + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason));
673             idpw.print(" used=");
674             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw);
675             idpw.print(" usedScr=");
676             TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
677             idpw.print(" lastPred=");
678             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw);
679             idpw.print(" activeLeft=");
680             TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime,
681                     idpw);
682             idpw.print(" wsLeft=");
683             TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime,
684                     idpw);
685             idpw.print(" lastJob=");
686             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
687             idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
688             idpw.println();
689         }
690         idpw.println();
691         idpw.print("totalElapsedTime=");
692         TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw);
693         idpw.println();
694         idpw.print("totalScreenOnTime=");
695         TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw);
696         idpw.println();
697         idpw.decreaseIndent();
698     }
699 }
700