• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy
6  * of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations
14  * under the License.
15  */
16 
17 package com.android.server.usage;
18 
19 import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT;
20 import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
21 import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK;
22 import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED;
23 import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT;
24 import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
25 import static android.app.usage.UsageStatsManager.REASON_SUB_MASK;
26 import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION;
27 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
28 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
29 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
30 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
31 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
32 
33 import static com.android.server.usage.AppStandbyController.isUserUsage;
34 
35 import android.annotation.CurrentTimeMillisLong;
36 import android.annotation.ElapsedRealtimeLong;
37 import android.app.usage.AppStandbyInfo;
38 import android.app.usage.UsageStatsManager;
39 import android.os.SystemClock;
40 import android.util.ArrayMap;
41 import android.util.ArraySet;
42 import android.util.AtomicFile;
43 import android.util.IndentingPrintWriter;
44 import android.util.Slog;
45 import android.util.SparseArray;
46 import android.util.SparseLongArray;
47 import android.util.TimeUtils;
48 import android.util.Xml;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.util.CollectionUtils;
52 import com.android.internal.util.FastXmlSerializer;
53 import com.android.internal.util.FrameworkStatsLog;
54 import com.android.internal.util.XmlUtils;
55 
56 import libcore.io.IoUtils;
57 
58 import org.xmlpull.v1.XmlPullParser;
59 import org.xmlpull.v1.XmlPullParserException;
60 
61 import java.io.BufferedOutputStream;
62 import java.io.BufferedReader;
63 import java.io.File;
64 import java.io.FileInputStream;
65 import java.io.FileNotFoundException;
66 import java.io.FileOutputStream;
67 import java.io.FileReader;
68 import java.io.IOException;
69 import java.nio.charset.StandardCharsets;
70 import java.util.ArrayList;
71 import java.util.List;
72 
73 /**
74  * Keeps track of recent active state changes in apps.
75  * Access should be guarded by a lock by the caller.
76  */
77 public class AppIdleHistory {
78 
79     private static final String TAG = "AppIdleHistory";
80 
81     private static final boolean DEBUG = AppStandbyController.DEBUG;
82 
83     // History for all users and all packages
84     private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>();
85     private static final long ONE_MINUTE = 60 * 1000;
86 
87     // Only keep the persisted restore-to-rare apps list for 2 days.
88     static final long RESTORE_TO_RARE_APPS_LIST_EXPIRY = ONE_MINUTE * 60 * 24 * 2;
89 
90     static final int STANDBY_BUCKET_UNKNOWN = -1;
91 
92     /**
93      * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are
94      * considered idle while those in higher buckets are not considered idle.
95      */
96     static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE;
97 
98     /** Initial version of the xml containing the app idle stats ({@link #APP_IDLE_FILENAME}). */
99     private static final int XML_VERSION_INITIAL = 0;
100     /**
101      * Allowed writing expiry times for any standby bucket instead of only active and working set.
102      * In previous version, we used to specify expiry times for active and working set as
103      * attributes:
104      * <pre>
105      *     <package activeTimeoutTime="..." workingSetTimeoutTime="..." />
106      * </pre>
107      * In this version, it is changed to:
108      * <pre>
109      *     <package>
110      *         <expiryTimes>
111      *             <item bucket="..." expiry="..." />
112      *             <item bucket="..." expiry="..." />
113      *         </expiryTimes>
114      *     </package>
115      * </pre>
116      */
117     private static final int XML_VERSION_ADD_BUCKET_EXPIRY_TIMES = 1;
118     /** Current version */
119     private static final int XML_VERSION_CURRENT = XML_VERSION_ADD_BUCKET_EXPIRY_TIMES;
120 
121     @VisibleForTesting
122     static final String APP_IDLE_FILENAME = "app_idle_stats.xml";
123     private static final String TAG_PACKAGES = "packages";
124     private static final String TAG_PACKAGE = "package";
125     private static final String TAG_BUCKET_EXPIRY_TIMES = "expiryTimes";
126     private static final String TAG_ITEM = "item";
127     private static final String ATTR_NAME = "name";
128     // Screen on timebase time when app was last used
129     private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
130     // Elapsed timebase time when app was last used
131     private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
132     // Elapsed timebase time when app was last used by the user
133     private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime";
134     // Elapsed timebase time when the app bucket was last predicted externally
135     private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime";
136     // The standby bucket for the app
137     private static final String ATTR_CURRENT_BUCKET = "appLimitBucket";
138     // The reason the app was put in the above bucket
139     private static final String ATTR_BUCKETING_REASON = "bucketReason";
140     // The last time a job was run for this app
141     private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime";
142     // The time when the forced active state can be overridden.
143     private static final String ATTR_BUCKET_ACTIVE_TIMEOUT_TIME = "activeTimeoutTime";
144     // The time when the forced working_set state can be overridden.
145     private static final String ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME = "workingSetTimeoutTime";
146     // The standby bucket value
147     private static final String ATTR_BUCKET = "bucket";
148     // The time when the forced bucket state can be overridde.
149     private static final String ATTR_EXPIRY_TIME = "expiry";
150     // Elapsed timebase time when the app was last marked for restriction.
151     private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED =
152             "lastRestrictionAttemptElapsedTime";
153     // Reason why the app was last marked for restriction.
154     private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON =
155             "lastRestrictionAttemptReason";
156     // The next estimated launch time of the app, in ms since epoch.
157     private static final String ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME = "nextEstimatedAppLaunchTime";
158     // Version of the xml file.
159     private static final String ATTR_VERSION = "version";
160 
161     // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
162     private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
163     private long mElapsedDuration; // Total device on duration since device was "born"
164 
165     // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot)
166     private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration
167     private long mScreenOnDuration; // Total screen on duration since device was "born"
168 
169     private final File mStorageDir;
170 
171     private boolean mScreenOn;
172 
173     static class AppUsageHistory {
174         // Last used time (including system usage), using elapsed timebase
175         long lastUsedElapsedTime;
176         // Last time the user used the app, using elapsed timebase
177         long lastUsedByUserElapsedTime;
178         // Last used time using screen_on timebase
179         long lastUsedScreenTime;
180         // Last predicted time using elapsed timebase
181         long lastPredictedTime;
182         // Last predicted bucket
183         @UsageStatsManager.StandbyBuckets
184         int lastPredictedBucket = STANDBY_BUCKET_UNKNOWN;
185         // Standby bucket
186         @UsageStatsManager.StandbyBuckets
187         int currentBucket;
188         // Reason for setting the standby bucket. The value here is a combination of
189         // one of UsageStatsManager.REASON_MAIN_* and one (or none) of
190         // UsageStatsManager.REASON_SUB_*. Also see REASON_MAIN_MASK and REASON_SUB_MASK.
191         int bucketingReason;
192         // In-memory only, last bucket for which the listeners were informed
193         int lastInformedBucket;
194         // The last time a job was run for this app, using elapsed timebase
195         long lastJobRunTime;
196         // The estimated time the app will be launched next, in milliseconds since epoch.
197         @CurrentTimeMillisLong
198         long nextEstimatedLaunchTime;
199         // Contains standby buckets that apps were forced into and the corresponding expiry times
200         // (in elapsed timebase) for each bucket state. App will stay in the highest bucket until
201         // it's expiry time is elapsed and will be moved to the next highest bucket.
202         SparseLongArray bucketExpiryTimesMs;
203         // The last time an agent attempted to put the app into the RESTRICTED bucket.
204         long lastRestrictAttemptElapsedTime;
205         // The last reason the app was marked to be put into the RESTRICTED bucket.
206         int lastRestrictReason;
207     }
208 
AppIdleHistory(File storageDir, long elapsedRealtime)209     AppIdleHistory(File storageDir, long elapsedRealtime) {
210         mElapsedSnapshot = elapsedRealtime;
211         mScreenOnSnapshot = elapsedRealtime;
212         mStorageDir = storageDir;
213         readScreenOnTime();
214     }
215 
updateDisplay(boolean screenOn, long elapsedRealtime)216     public void updateDisplay(boolean screenOn, long elapsedRealtime) {
217         if (screenOn == mScreenOn) return;
218 
219         mScreenOn = screenOn;
220         if (mScreenOn) {
221             mScreenOnSnapshot = elapsedRealtime;
222         } else {
223             mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot;
224             mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
225             mElapsedSnapshot = elapsedRealtime;
226         }
227         if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot
228                 + ", mScreenOnDuration=" + mScreenOnDuration
229                 + ", mScreenOn=" + mScreenOn);
230     }
231 
getScreenOnTime(long elapsedRealtime)232     public long getScreenOnTime(long elapsedRealtime) {
233         long screenOnTime = mScreenOnDuration;
234         if (mScreenOn) {
235             screenOnTime += elapsedRealtime - mScreenOnSnapshot;
236         }
237         return screenOnTime;
238     }
239 
240     @VisibleForTesting
getScreenOnTimeFile()241     File getScreenOnTimeFile() {
242         return new File(mStorageDir, "screen_on_time");
243     }
244 
readScreenOnTime()245     private void readScreenOnTime() {
246         File screenOnTimeFile = getScreenOnTimeFile();
247         if (screenOnTimeFile.exists()) {
248             try {
249                 BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile));
250                 mScreenOnDuration = Long.parseLong(reader.readLine());
251                 mElapsedDuration = Long.parseLong(reader.readLine());
252                 reader.close();
253             } catch (IOException | NumberFormatException e) {
254             }
255         } else {
256             writeScreenOnTime();
257         }
258     }
259 
writeScreenOnTime()260     private void writeScreenOnTime() {
261         AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile());
262         FileOutputStream fos = null;
263         try {
264             fos = screenOnTimeFile.startWrite();
265             fos.write((Long.toString(mScreenOnDuration) + "\n"
266                     + Long.toString(mElapsedDuration) + "\n").getBytes());
267             screenOnTimeFile.finishWrite(fos);
268         } catch (IOException ioe) {
269             screenOnTimeFile.failWrite(fos);
270         }
271     }
272 
273     /**
274      * To be called periodically to keep track of elapsed time when app idle times are written
275      */
writeAppIdleDurations()276     public void writeAppIdleDurations() {
277         final long elapsedRealtime = SystemClock.elapsedRealtime();
278         // Only bump up and snapshot the elapsed time. Don't change screen on duration.
279         mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
280         mElapsedSnapshot = elapsedRealtime;
281         writeScreenOnTime();
282     }
283 
getRestoreToRareAppsListFile(int userId)284     private File getRestoreToRareAppsListFile(int userId) {
285         return new File(getUserDirectory(userId), "restore_to_rare_apps_list");
286     }
287 
readRestoreToRareAppsList(int userId)288     public ArraySet<String> readRestoreToRareAppsList(int userId) {
289         File restoreToRareAppsListFile = getRestoreToRareAppsListFile(userId);
290         if (!restoreToRareAppsListFile.exists()) {
291             return null;
292         }
293 
294         try (BufferedReader reader =
295                      new BufferedReader(new FileReader(restoreToRareAppsListFile))) {
296             final ArraySet<String> appsList = new ArraySet<>();
297             final long restoreTime = Long.parseLong(reader.readLine());
298             if (System.currentTimeMillis() - restoreTime > RESTORE_TO_RARE_APPS_LIST_EXPIRY) {
299                 // the apps list should only be kept around for 2 days
300                 reader.close();
301                 restoreToRareAppsListFile.delete();
302                 return null;
303             }
304             String pkgName;
305             while ((pkgName = reader.readLine()) != null) {
306                 appsList.add(pkgName);
307             }
308             return appsList;
309         } catch (IOException | NumberFormatException e) {
310             return null;
311         }
312     }
313 
writeRestoreToRareAppsList(int userId, ArraySet<String> restoreAppsToRare)314     public void writeRestoreToRareAppsList(int userId, ArraySet<String> restoreAppsToRare) {
315         File fileHandle = getRestoreToRareAppsListFile(userId);
316         if (fileHandle.exists()) {
317             // don't update the persisted file - it should only be written once.
318             return;
319         }
320         AtomicFile restoreToRareAppsListFile = new AtomicFile(fileHandle);
321         FileOutputStream fos = null;
322         try {
323             fos = restoreToRareAppsListFile.startWrite();
324             final StringBuilder sb = new StringBuilder();
325             sb.append(System.currentTimeMillis()).append("\n");
326             for (String pkgName : restoreAppsToRare) {
327                 sb.append(pkgName).append("\n");
328             }
329             fos.write(sb.toString().getBytes());
330             restoreToRareAppsListFile.finishWrite(fos);
331         } catch (IOException ioe) {
332             restoreToRareAppsListFile.failWrite(fos);
333         }
334     }
335 
336     /**
337      * Mark the app as used and update the bucket if necessary. If there is a expiry time specified
338      * that's in the future, then the usage event is temporary and keeps the app in the specified
339      * bucket at least until the expiry time is reached. This can be used to keep the app in an
340      * elevated bucket for a while until some important task gets to run.
341      *
342      * @param appUsageHistory the usage record for the app being updated
343      * @param packageName name of the app being updated, for logging purposes
344      * @param newBucket the bucket to set the app to
345      * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_*
346      * @param nowElapsedRealtimeMs mark as used time if non-zero (in
347      *                          {@link SystemClock#elapsedRealtime()} time base)
348      * @param expiryElapsedRealtimeMs the expiry time for the specified bucket (in
349      *                         {@link SystemClock#elapsedRealtime()} time base)
350      * @return {@code appUsageHistory}
351      */
reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, int newBucket, int usageReason, long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs)352     AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId,
353             int newBucket, int usageReason,
354             long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs) {
355         int bucketingReason = REASON_MAIN_USAGE | usageReason;
356         final boolean isUserUsage = isUserUsage(bucketingReason);
357 
358         if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage
359                 && (appUsageHistory.bucketingReason & REASON_MAIN_MASK) != REASON_MAIN_TIMEOUT) {
360             // Only user usage should bring an app out of the RESTRICTED bucket, unless the app
361             // just timed out into RESTRICTED.
362             newBucket = STANDBY_BUCKET_RESTRICTED;
363             bucketingReason = appUsageHistory.bucketingReason;
364         } else {
365             // Set the expiry time if applicable
366             if (expiryElapsedRealtimeMs > nowElapsedRealtimeMs) {
367                 // Convert to elapsed timebase
368                 final long expiryTimeMs = getElapsedTime(expiryElapsedRealtimeMs);
369                 if (appUsageHistory.bucketExpiryTimesMs == null) {
370                     appUsageHistory.bucketExpiryTimesMs = new SparseLongArray();
371                 }
372                 final long currentExpiryTimeMs = appUsageHistory.bucketExpiryTimesMs.get(newBucket);
373                 appUsageHistory.bucketExpiryTimesMs.put(newBucket,
374                         Math.max(expiryTimeMs, currentExpiryTimeMs));
375                 removeElapsedExpiryTimes(appUsageHistory, getElapsedTime(nowElapsedRealtimeMs));
376             }
377         }
378 
379         if (nowElapsedRealtimeMs != 0) {
380             appUsageHistory.lastUsedElapsedTime = mElapsedDuration
381                     + (nowElapsedRealtimeMs - mElapsedSnapshot);
382             if (isUserUsage) {
383                 appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime;
384             }
385             appUsageHistory.lastUsedScreenTime = getScreenOnTime(nowElapsedRealtimeMs);
386         }
387 
388         if (appUsageHistory.currentBucket >= newBucket) {
389             if (appUsageHistory.currentBucket > newBucket) {
390                 appUsageHistory.currentBucket = newBucket;
391                 logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason);
392             }
393             appUsageHistory.bucketingReason = bucketingReason;
394         }
395 
396         return appUsageHistory;
397     }
398 
removeElapsedExpiryTimes(AppUsageHistory appUsageHistory, long elapsedTimeMs)399     private void removeElapsedExpiryTimes(AppUsageHistory appUsageHistory, long elapsedTimeMs) {
400         if (appUsageHistory.bucketExpiryTimesMs == null) {
401             return;
402         }
403         for (int i = appUsageHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) {
404             if (appUsageHistory.bucketExpiryTimesMs.valueAt(i) < elapsedTimeMs) {
405                 appUsageHistory.bucketExpiryTimesMs.removeAt(i);
406             }
407         }
408     }
409 
410     /**
411      * Mark the app as used and update the bucket if necessary. If there is a expiry time specified
412      * that's in the future, then the usage event is temporary and keeps the app in the specified
413      * bucket at least until the expiry time is reached. This can be used to keep the app in an
414      * elevated bucket for a while until some important task gets to run.
415      *
416      * @param packageName package name of the app the usage is reported for
417      * @param userId user that the app is running in
418      * @param newBucket the bucket to set the app to
419      * @param usageReason sub reason for usage
420      * @param nowElapsedRealtimeMs mark as used time if non-zero (in
421      *                             {@link SystemClock#elapsedRealtime()} time base).
422      * @param expiryElapsedRealtimeMs the expiry time for the specified bucket (in
423      *                         {@link SystemClock#elapsedRealtime()} time base).
424      * @return the {@link AppUsageHistory} corresponding to the {@code packageName}
425      *         and {@code userId}.
426      */
reportUsage(String packageName, int userId, int newBucket, int usageReason, long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs)427     public AppUsageHistory reportUsage(String packageName, int userId, int newBucket,
428             int usageReason, long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs) {
429         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
430         AppUsageHistory history = getPackageHistory(userHistory, packageName,
431                 nowElapsedRealtimeMs, true);
432         return reportUsage(history, packageName, userId, newBucket, usageReason,
433                 nowElapsedRealtimeMs, expiryElapsedRealtimeMs);
434     }
435 
getUserHistory(int userId)436     private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) {
437         ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
438         if (userHistory == null) {
439             userHistory = new ArrayMap<>();
440             mIdleHistory.put(userId, userHistory);
441             readAppIdleTimes(userId, userHistory);
442         }
443         return userHistory;
444     }
445 
446     // TODO (206518483): Remove unused parameter 'elapsedRealtime'.
getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory, String packageName, long elapsedRealtime, boolean create)447     private AppUsageHistory getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory,
448             String packageName, long elapsedRealtime, boolean create) {
449         AppUsageHistory appUsageHistory = userHistory.get(packageName);
450         if (appUsageHistory == null && create) {
451             appUsageHistory = new AppUsageHistory();
452             appUsageHistory.lastUsedByUserElapsedTime = Integer.MIN_VALUE;
453             appUsageHistory.lastUsedElapsedTime = Integer.MIN_VALUE;
454             appUsageHistory.lastUsedScreenTime = Integer.MIN_VALUE;
455             appUsageHistory.lastPredictedTime = Integer.MIN_VALUE;
456             appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER;
457             appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT;
458             appUsageHistory.lastInformedBucket = -1;
459             appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago
460             userHistory.put(packageName, appUsageHistory);
461         }
462         return appUsageHistory;
463     }
464 
onUserRemoved(int userId)465     public void onUserRemoved(int userId) {
466         mIdleHistory.remove(userId);
467     }
468 
isIdle(String packageName, int userId, long elapsedRealtime)469     public boolean isIdle(String packageName, int userId, long elapsedRealtime) {
470         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
471         AppUsageHistory appUsageHistory =
472                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
473         return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF;
474     }
475 
getAppUsageHistory(String packageName, int userId, long elapsedRealtime)476     public AppUsageHistory getAppUsageHistory(String packageName, int userId,
477             long elapsedRealtime) {
478         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
479         AppUsageHistory appUsageHistory =
480                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
481         return appUsageHistory;
482     }
483 
setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason)484     public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
485             int bucket, int reason) {
486         setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false);
487     }
488 
setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason, boolean resetExpiryTimes)489     public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
490             int bucket, int reason, boolean resetExpiryTimes) {
491         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
492         AppUsageHistory appUsageHistory =
493                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
494         final boolean changed = appUsageHistory.currentBucket != bucket;
495         appUsageHistory.currentBucket = bucket;
496         appUsageHistory.bucketingReason = reason;
497 
498         final long elapsed = getElapsedTime(elapsedRealtime);
499 
500         if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) {
501             appUsageHistory.lastPredictedTime = elapsed;
502             appUsageHistory.lastPredictedBucket = bucket;
503         }
504         if (resetExpiryTimes && appUsageHistory.bucketExpiryTimesMs != null) {
505             appUsageHistory.bucketExpiryTimesMs.clear();
506         }
507         if (changed) {
508             logAppStandbyBucketChanged(packageName, userId, bucket, reason);
509         }
510     }
511 
512     /**
513      * Update the prediction for the app but don't change the actual bucket
514      * @param app The app for which the prediction was made
515      * @param elapsedTimeAdjusted The elapsed time in the elapsed duration timebase
516      * @param bucket The predicted bucket
517      */
updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket)518     public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) {
519         app.lastPredictedTime = elapsedTimeAdjusted;
520         app.lastPredictedBucket = bucket;
521     }
522 
523     /**
524      * Marks the next time the app is expected to be launched, in the current millis timebase.
525      */
setEstimatedLaunchTime(String packageName, int userId, @ElapsedRealtimeLong long nowElapsed, @CurrentTimeMillisLong long launchTime)526     public void setEstimatedLaunchTime(String packageName, int userId,
527             @ElapsedRealtimeLong long nowElapsed, @CurrentTimeMillisLong long launchTime) {
528         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
529         AppUsageHistory appUsageHistory =
530                 getPackageHistory(userHistory, packageName, nowElapsed, true);
531         appUsageHistory.nextEstimatedLaunchTime = launchTime;
532     }
533 
534     /**
535      * Marks the last time a job was run, with the given elapsedRealtime. The time stored is
536      * based on the elapsed timebase.
537      * @param packageName
538      * @param userId
539      * @param elapsedRealtime
540      */
setLastJobRunTime(String packageName, int userId, long elapsedRealtime)541     public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
542         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
543         AppUsageHistory appUsageHistory =
544                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
545         appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime);
546     }
547 
548     /**
549      * Notes an attempt to put the app in the {@link UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
550      * bucket.
551      *
552      * @param packageName     The package name of the app that is being restricted
553      * @param userId          The ID of the user in which the app is being restricted
554      * @param elapsedRealtime The time the attempt was made, in the (unadjusted) elapsed realtime
555      *                        timebase
556      * @param reason          The reason for the restriction attempt
557      */
noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason)558     void noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason) {
559         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
560         AppUsageHistory appUsageHistory =
561                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
562         appUsageHistory.lastRestrictAttemptElapsedTime = getElapsedTime(elapsedRealtime);
563         appUsageHistory.lastRestrictReason = reason;
564     }
565 
566     /**
567      * Returns the next estimated launch time of this app. Will return {@link Long#MAX_VALUE} if
568      * there's no estimated time.
569      */
570     @CurrentTimeMillisLong
getEstimatedLaunchTime(String packageName, int userId, long nowElapsed)571     public long getEstimatedLaunchTime(String packageName, int userId, long nowElapsed) {
572         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
573         AppUsageHistory appUsageHistory =
574                 getPackageHistory(userHistory, packageName, nowElapsed, false);
575         // Don't adjust the default, else it'll wrap around to a positive value
576         if (appUsageHistory == null
577                 || appUsageHistory.nextEstimatedLaunchTime < System.currentTimeMillis()) {
578             return Long.MAX_VALUE;
579         }
580         return appUsageHistory.nextEstimatedLaunchTime;
581     }
582 
583     /**
584      * Returns the time since the last job was run for this app. This can be larger than the
585      * current elapsedRealtime, in case it happened before boot or a really large value if no jobs
586      * were ever run.
587      * @param packageName
588      * @param userId
589      * @param elapsedRealtime
590      * @return
591      */
getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime)592     public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) {
593         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
594         AppUsageHistory appUsageHistory =
595                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
596         // Don't adjust the default, else it'll wrap around to a positive value
597         if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) {
598             return Long.MAX_VALUE;
599         }
600         return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime;
601     }
602 
getTimeSinceLastUsedByUser(String packageName, int userId, long elapsedRealtime)603     public long getTimeSinceLastUsedByUser(String packageName, int userId, long elapsedRealtime) {
604         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
605         AppUsageHistory appUsageHistory =
606                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
607         if (appUsageHistory == null || appUsageHistory.lastUsedByUserElapsedTime == Long.MIN_VALUE
608                 || appUsageHistory.lastUsedByUserElapsedTime <= 0) {
609             return Long.MAX_VALUE;
610         }
611         return getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedByUserElapsedTime;
612     }
613 
getAppStandbyBucket(String packageName, int userId, long elapsedRealtime)614     public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) {
615         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
616         AppUsageHistory appUsageHistory =
617                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
618         return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket;
619     }
620 
getAppStandbyBuckets(int userId, boolean appIdleEnabled)621     public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) {
622         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
623         int size = userHistory.size();
624         ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size);
625         for (int i = 0; i < size; i++) {
626             buckets.add(new AppStandbyInfo(userHistory.keyAt(i),
627                     appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE));
628         }
629         return buckets;
630     }
631 
getAppStandbyReason(String packageName, int userId, long elapsedRealtime)632     public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) {
633         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
634         AppUsageHistory appUsageHistory =
635                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
636         return appUsageHistory != null ? appUsageHistory.bucketingReason : 0;
637     }
638 
getElapsedTime(long elapsedRealtime)639     public long getElapsedTime(long elapsedRealtime) {
640         return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
641     }
642 
643     /* Returns the new standby bucket the app is assigned to */
setIdle(String packageName, int userId, boolean idle, long elapsedRealtime)644     public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
645         final int newBucket;
646         final int reason;
647         if (idle) {
648             newBucket = IDLE_BUCKET_CUTOFF;
649             reason = REASON_MAIN_FORCED_BY_USER;
650             final AppUsageHistory appHistory = getAppUsageHistory(packageName, userId,
651                     elapsedRealtime);
652             // Wipe all expiry times that could raise the bucket on reevaluation.
653             if (appHistory.bucketExpiryTimesMs != null) {
654                 for (int i = appHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) {
655                     if (appHistory.bucketExpiryTimesMs.keyAt(i) < newBucket) {
656                         appHistory.bucketExpiryTimesMs.removeAt(i);
657                     }
658                 }
659             }
660         } else {
661             newBucket = STANDBY_BUCKET_ACTIVE;
662             // This is to pretend that the app was just used, don't freeze the state anymore.
663             reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION;
664         }
665         setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, false);
666 
667         return newBucket;
668     }
669 
clearUsage(String packageName, int userId)670     public void clearUsage(String packageName, int userId) {
671         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
672         userHistory.remove(packageName);
673     }
674 
shouldInformListeners(String packageName, int userId, long elapsedRealtime, int bucket)675     boolean shouldInformListeners(String packageName, int userId,
676             long elapsedRealtime, int bucket) {
677         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
678         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
679                 elapsedRealtime, true);
680         if (appUsageHistory.lastInformedBucket != bucket) {
681             appUsageHistory.lastInformedBucket = bucket;
682             return true;
683         }
684         return false;
685     }
686 
687     /**
688      * Returns the index in the array of elapsedTimeThresholds that corresponds to
689      * how long since the app was used.
690      * @param packageName
691      * @param userId
692      * @param elapsedRealtime current time
693      * @param screenTimeThresholds Array of screen times, in ascending order,
694      *        first one is 0 (this will not be used any more)
695      * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0
696      * @return The index whose values the app's used time exceeds or {@code -1} to
697      *         indicate that the app has never been used.
698      */
getThresholdIndex(String packageName, int userId, long elapsedRealtime, long[] screenTimeThresholds, long[] elapsedTimeThresholds)699     int getThresholdIndex(String packageName, int userId, long elapsedRealtime,
700             long[] screenTimeThresholds, long[] elapsedTimeThresholds) {
701         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
702         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
703                 elapsedRealtime, false);
704         // If we don't have any state for the app, assume never used
705         if (appUsageHistory == null || appUsageHistory.lastUsedElapsedTime < 0
706                 || (!Flags.screenTimeBypass() && appUsageHistory.lastUsedScreenTime < 0)) {
707             return -1;
708         }
709 
710         long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
711         long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
712 
713         if (DEBUG) Slog.d(TAG, packageName
714                 + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime
715                 + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime);
716         if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta
717                 + ", elapsed=" + elapsedDelta);
718         for (int i = screenTimeThresholds.length - 1; i >= 0; i--) {
719             if ((Flags.screenTimeBypass() || screenOnDelta >= screenTimeThresholds[i])
720                 && elapsedDelta >= elapsedTimeThresholds[i]) {
721                 return i;
722             }
723         }
724         return 0;
725     }
726 
727     /**
728      * Log a standby bucket change to statsd, and also logcat if debug logging is enabled.
729      */
logAppStandbyBucketChanged(String packageName, int userId, int bucket, int reason)730     private void logAppStandbyBucketChanged(String packageName, int userId, int bucket,
731             int reason) {
732         FrameworkStatsLog.write(
733                 FrameworkStatsLog.APP_STANDBY_BUCKET_CHANGED,
734                 packageName, userId, bucket,
735                 (reason & REASON_MAIN_MASK), (reason & REASON_SUB_MASK));
736         if (DEBUG) {
737             Slog.d(TAG, "Moved " + packageName + " to bucket=" + bucket
738                     + ", reason=0x0" + Integer.toHexString(reason));
739         }
740     }
741 
742     @VisibleForTesting
getBucketExpiryTimeMs(String packageName, int userId, int bucket, long elapsedRealtimeMs)743     long getBucketExpiryTimeMs(String packageName, int userId, int bucket, long elapsedRealtimeMs) {
744         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
745         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
746                 elapsedRealtimeMs, false /* create */);
747         if (appUsageHistory == null || appUsageHistory.bucketExpiryTimesMs == null) {
748             return 0;
749         }
750         return appUsageHistory.bucketExpiryTimesMs.get(bucket, 0);
751     }
752 
getUserDirectory(int userId)753     private File getUserDirectory(int userId) {
754         return new File(new File(mStorageDir, "users"), Integer.toString(userId));
755     }
756 
757     @VisibleForTesting
getUserFile(int userId)758     File getUserFile(int userId) {
759         return new File(getUserDirectory(userId), APP_IDLE_FILENAME);
760     }
761 
clearLastUsedTimestamps(String packageName, int userId)762     void clearLastUsedTimestamps(String packageName, int userId) {
763         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
764         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
765                 SystemClock.elapsedRealtime(), false /* create */);
766         if (appUsageHistory != null) {
767             appUsageHistory.lastUsedByUserElapsedTime = Integer.MIN_VALUE;
768             appUsageHistory.lastUsedElapsedTime = Integer.MIN_VALUE;
769             appUsageHistory.lastUsedScreenTime = Integer.MIN_VALUE;
770         }
771     }
772 
773     /**
774      * Check if App Idle File exists on disk
775      * @param userId
776      * @return true if file exists
777      */
userFileExists(int userId)778     public boolean userFileExists(int userId) {
779         return getUserFile(userId).exists();
780     }
781 
readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory)782     private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) {
783         FileInputStream fis = null;
784         try {
785             AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
786             fis = appIdleFile.openRead();
787             XmlPullParser parser = Xml.newPullParser();
788             parser.setInput(fis, StandardCharsets.UTF_8.name());
789 
790             int type;
791             while ((type = parser.next()) != XmlPullParser.START_TAG
792                     && type != XmlPullParser.END_DOCUMENT) {
793                 // Skip
794             }
795 
796             if (type != XmlPullParser.START_TAG) {
797                 Slog.e(TAG, "Unable to read app idle file for user " + userId);
798                 return;
799             }
800             if (!parser.getName().equals(TAG_PACKAGES)) {
801                 return;
802             }
803             final int version = getIntValue(parser, ATTR_VERSION, XML_VERSION_INITIAL);
804             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
805                 if (type == XmlPullParser.START_TAG) {
806                     final String name = parser.getName();
807                     if (name.equals(TAG_PACKAGE)) {
808                         final String packageName = parser.getAttributeValue(null, ATTR_NAME);
809                         AppUsageHistory appUsageHistory = new AppUsageHistory();
810                         appUsageHistory.lastUsedElapsedTime =
811                                 Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
812                         appUsageHistory.lastUsedByUserElapsedTime = getLongValue(parser,
813                                 ATTR_LAST_USED_BY_USER_ELAPSED,
814                                 appUsageHistory.lastUsedElapsedTime);
815                         appUsageHistory.lastUsedScreenTime =
816                                 Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
817                         appUsageHistory.lastPredictedTime = getLongValue(parser,
818                                 ATTR_LAST_PREDICTED_TIME, 0L);
819                         String currentBucketString = parser.getAttributeValue(null,
820                                 ATTR_CURRENT_BUCKET);
821                         appUsageHistory.currentBucket = currentBucketString == null
822                                 ? STANDBY_BUCKET_ACTIVE
823                                 : Integer.parseInt(currentBucketString);
824                         String bucketingReason =
825                                 parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
826                         appUsageHistory.lastJobRunTime = getLongValue(parser,
827                                 ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE);
828                         appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT;
829                         if (bucketingReason != null) {
830                             try {
831                                 appUsageHistory.bucketingReason =
832                                         Integer.parseInt(bucketingReason, 16);
833                             } catch (NumberFormatException nfe) {
834                                 Slog.wtf(TAG, "Unable to read bucketing reason", nfe);
835                             }
836                         }
837                         appUsageHistory.lastRestrictAttemptElapsedTime =
838                                 getLongValue(parser, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 0);
839                         String lastRestrictReason = parser.getAttributeValue(
840                                 null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON);
841                         if (lastRestrictReason != null) {
842                             try {
843                                 appUsageHistory.lastRestrictReason =
844                                         Integer.parseInt(lastRestrictReason, 16);
845                             } catch (NumberFormatException nfe) {
846                                 Slog.wtf(TAG, "Unable to read last restrict reason", nfe);
847                             }
848                         }
849                         appUsageHistory.nextEstimatedLaunchTime = getLongValue(parser,
850                                 ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME, 0);
851                         if (Flags.avoidIdleCheck()) {
852                             // Set lastInformedBucket to the same value with the currentBucket
853                             // it should have already been informed.
854                             appUsageHistory.lastInformedBucket = appUsageHistory.currentBucket;
855                         } else {
856                             appUsageHistory.lastInformedBucket = -1;
857                         }
858                         userHistory.put(packageName, appUsageHistory);
859 
860                         if (version >= XML_VERSION_ADD_BUCKET_EXPIRY_TIMES) {
861                             final int outerDepth = parser.getDepth();
862                             while (XmlUtils.nextElementWithin(parser, outerDepth)) {
863                                 if (TAG_BUCKET_EXPIRY_TIMES.equals(parser.getName())) {
864                                     readBucketExpiryTimes(parser, appUsageHistory);
865                                 }
866                             }
867                         } else {
868                             final long bucketActiveTimeoutTime = getLongValue(parser,
869                                     ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L);
870                             final long bucketWorkingSetTimeoutTime = getLongValue(parser,
871                                     ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L);
872                             if (bucketActiveTimeoutTime != 0 || bucketWorkingSetTimeoutTime != 0) {
873                                 insertBucketExpiryTime(appUsageHistory,
874                                         STANDBY_BUCKET_ACTIVE, bucketActiveTimeoutTime);
875                                 insertBucketExpiryTime(appUsageHistory,
876                                         STANDBY_BUCKET_WORKING_SET, bucketWorkingSetTimeoutTime);
877                             }
878                         }
879                     }
880                 }
881             }
882         } catch (FileNotFoundException e) {
883             // Expected on first boot
884             Slog.d(TAG, "App idle file for user " + userId + " does not exist");
885         } catch (IOException | XmlPullParserException e) {
886             Slog.e(TAG, "Unable to read app idle file for user " + userId, e);
887         } finally {
888             IoUtils.closeQuietly(fis);
889         }
890     }
891 
readBucketExpiryTimes(XmlPullParser parser, AppUsageHistory appUsageHistory)892     private void readBucketExpiryTimes(XmlPullParser parser, AppUsageHistory appUsageHistory)
893             throws IOException, XmlPullParserException {
894         final int depth = parser.getDepth();
895         while (XmlUtils.nextElementWithin(parser, depth)) {
896             if (TAG_ITEM.equals(parser.getName())) {
897                 final int bucket = getIntValue(parser, ATTR_BUCKET, STANDBY_BUCKET_UNKNOWN);
898                 if (bucket == STANDBY_BUCKET_UNKNOWN) {
899                     Slog.e(TAG, "Error reading the buckets expiry times");
900                     continue;
901                 }
902                 final long expiryTimeMs = getLongValue(parser, ATTR_EXPIRY_TIME, 0 /* default */);
903                 insertBucketExpiryTime(appUsageHistory, bucket, expiryTimeMs);
904             }
905         }
906     }
907 
insertBucketExpiryTime(AppUsageHistory appUsageHistory, int bucket, long expiryTimeMs)908     private void insertBucketExpiryTime(AppUsageHistory appUsageHistory,
909             int bucket, long expiryTimeMs) {
910         if (expiryTimeMs == 0) {
911             return;
912         }
913         if (appUsageHistory.bucketExpiryTimesMs == null) {
914             appUsageHistory.bucketExpiryTimesMs = new SparseLongArray();
915         }
916         appUsageHistory.bucketExpiryTimesMs.put(bucket, expiryTimeMs);
917     }
918 
getLongValue(XmlPullParser parser, String attrName, long defValue)919     private long getLongValue(XmlPullParser parser, String attrName, long defValue) {
920         String value = parser.getAttributeValue(null, attrName);
921         if (value == null) return defValue;
922         return Long.parseLong(value);
923     }
924 
getIntValue(XmlPullParser parser, String attrName, int defValue)925     private int getIntValue(XmlPullParser parser, String attrName, int defValue) {
926         String value = parser.getAttributeValue(null, attrName);
927         if (value == null) return defValue;
928         return Integer.parseInt(value);
929     }
930 
writeAppIdleTimes(long elapsedRealtimeMs)931     public void writeAppIdleTimes(long elapsedRealtimeMs) {
932         final int size = mIdleHistory.size();
933         for (int i = 0; i < size; i++) {
934             writeAppIdleTimes(mIdleHistory.keyAt(i), elapsedRealtimeMs);
935         }
936     }
937 
writeAppIdleTimes(int userId, long elapsedRealtimeMs)938     public void writeAppIdleTimes(int userId, long elapsedRealtimeMs) {
939         FileOutputStream fos = null;
940         AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
941         try {
942             fos = appIdleFile.startWrite();
943             final BufferedOutputStream bos = new BufferedOutputStream(fos);
944 
945             FastXmlSerializer xml = new FastXmlSerializer();
946             xml.setOutput(bos, StandardCharsets.UTF_8.name());
947             xml.startDocument(null, true);
948             xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
949 
950             xml.startTag(null, TAG_PACKAGES);
951             xml.attribute(null, ATTR_VERSION, String.valueOf(XML_VERSION_CURRENT));
952 
953             final long elapsedTimeMs = getElapsedTime(elapsedRealtimeMs);
954             ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId);
955             final int N = userHistory.size();
956             for (int i = 0; i < N; i++) {
957                 String packageName = userHistory.keyAt(i);
958                 // Skip any unexpected null package names
959                 if (packageName == null) {
960                     Slog.w(TAG, "Skipping App Idle write for unexpected null package");
961                     continue;
962                 }
963                 AppUsageHistory history = userHistory.valueAt(i);
964                 xml.startTag(null, TAG_PACKAGE);
965                 xml.attribute(null, ATTR_NAME, packageName);
966                 xml.attribute(null, ATTR_ELAPSED_IDLE,
967                         Long.toString(history.lastUsedElapsedTime));
968                 xml.attribute(null, ATTR_LAST_USED_BY_USER_ELAPSED,
969                         Long.toString(history.lastUsedByUserElapsedTime));
970                 xml.attribute(null, ATTR_SCREEN_IDLE,
971                         Long.toString(history.lastUsedScreenTime));
972                 xml.attribute(null, ATTR_LAST_PREDICTED_TIME,
973                         Long.toString(history.lastPredictedTime));
974                 xml.attribute(null, ATTR_CURRENT_BUCKET,
975                         Integer.toString(history.currentBucket));
976                 xml.attribute(null, ATTR_BUCKETING_REASON,
977                         Integer.toHexString(history.bucketingReason));
978                 if (history.lastJobRunTime != Long.MIN_VALUE) {
979                     xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history
980                             .lastJobRunTime));
981                 }
982                 if (history.lastRestrictAttemptElapsedTime > 0) {
983                     xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED,
984                             Long.toString(history.lastRestrictAttemptElapsedTime));
985                 }
986                 xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON,
987                         Integer.toHexString(history.lastRestrictReason));
988                 if (history.nextEstimatedLaunchTime > 0) {
989                     xml.attribute(null, ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME,
990                             Long.toString(history.nextEstimatedLaunchTime));
991                 }
992                 if (history.bucketExpiryTimesMs != null) {
993                     xml.startTag(null, TAG_BUCKET_EXPIRY_TIMES);
994                     final int size = history.bucketExpiryTimesMs.size();
995                     for (int j = 0; j < size; ++j) {
996                         final long expiryTimeMs = history.bucketExpiryTimesMs.valueAt(j);
997                         // Skip writing to disk if the expiry time already elapsed.
998                         if (expiryTimeMs < elapsedTimeMs) {
999                             continue;
1000                         }
1001                         final int bucket = history.bucketExpiryTimesMs.keyAt(j);
1002                         xml.startTag(null, TAG_ITEM);
1003                         xml.attribute(null, ATTR_BUCKET, String.valueOf(bucket));
1004                         xml.attribute(null, ATTR_EXPIRY_TIME, String.valueOf(expiryTimeMs));
1005                         xml.endTag(null, TAG_ITEM);
1006                     }
1007                     xml.endTag(null, TAG_BUCKET_EXPIRY_TIMES);
1008                 }
1009                 xml.endTag(null, TAG_PACKAGE);
1010             }
1011 
1012             xml.endTag(null, TAG_PACKAGES);
1013             xml.endDocument();
1014             appIdleFile.finishWrite(fos);
1015         } catch (Exception e) {
1016             appIdleFile.failWrite(fos);
1017             Slog.e(TAG, "Error writing app idle file for user " + userId, e);
1018         }
1019     }
1020 
dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs)1021     public void dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) {
1022         final int numUsers = userIds.length;
1023         for (int i = 0; i < numUsers; i++) {
1024             idpw.println();
1025             dumpUser(idpw, userIds[i], pkgs);
1026         }
1027     }
1028 
dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs)1029     private void dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs) {
1030         idpw.print("User ");
1031         idpw.print(userId);
1032         idpw.println(" App Standby States:");
1033         idpw.increaseIndent();
1034         ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
1035         final long now = System.currentTimeMillis();
1036         final long elapsedRealtime = SystemClock.elapsedRealtime();
1037         final long totalElapsedTime = getElapsedTime(elapsedRealtime);
1038         final long screenOnTime = getScreenOnTime(elapsedRealtime);
1039         if (userHistory == null) return;
1040         final int P = userHistory.size();
1041         for (int p = 0; p < P; p++) {
1042             final String packageName = userHistory.keyAt(p);
1043             final AppUsageHistory appUsageHistory = userHistory.valueAt(p);
1044             if (!CollectionUtils.isEmpty(pkgs) && !pkgs.contains(packageName)) {
1045                 continue;
1046             }
1047             idpw.print("package=" + packageName);
1048             idpw.print(" u=" + userId);
1049             idpw.print(" bucket=" + appUsageHistory.currentBucket
1050                     + " reason="
1051                     + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason));
1052             idpw.print(" used=");
1053             printLastActionElapsedTime(idpw, totalElapsedTime, appUsageHistory.lastUsedElapsedTime);
1054             idpw.print(" usedByUser=");
1055             printLastActionElapsedTime(idpw, totalElapsedTime,
1056                     appUsageHistory.lastUsedByUserElapsedTime);
1057             idpw.print(" usedScr=");
1058             printLastActionElapsedTime(idpw, totalElapsedTime, appUsageHistory.lastUsedScreenTime);
1059             idpw.print(" lastPred=");
1060             printLastActionElapsedTime(idpw, totalElapsedTime, appUsageHistory.lastPredictedTime);
1061             dumpBucketExpiryTimes(idpw, appUsageHistory, totalElapsedTime);
1062             idpw.print(" lastJob=");
1063             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
1064             idpw.print(" lastInformedBucket=" + appUsageHistory.lastInformedBucket);
1065             if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) {
1066                 idpw.print(" lastRestrictAttempt=");
1067                 TimeUtils.formatDuration(
1068                         totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw);
1069                 idpw.print(" lastRestrictReason="
1070                         + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason));
1071             }
1072             if (appUsageHistory.nextEstimatedLaunchTime > 0) {
1073                 idpw.print(" nextEstimatedLaunchTime=");
1074                 TimeUtils.formatDuration(appUsageHistory.nextEstimatedLaunchTime - now, idpw);
1075             }
1076             idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
1077             idpw.println();
1078         }
1079         idpw.println();
1080         idpw.print("totalElapsedTime=");
1081         TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw);
1082         idpw.println();
1083         idpw.print("totalScreenOnTime=");
1084         TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw);
1085         idpw.println();
1086         idpw.decreaseIndent();
1087     }
1088 
printLastActionElapsedTime(IndentingPrintWriter idpw, long totalElapsedTimeMS, long lastActionTimeMs)1089     private void printLastActionElapsedTime(IndentingPrintWriter idpw, long totalElapsedTimeMS,
1090             long lastActionTimeMs) {
1091         if (lastActionTimeMs < 0) {
1092             idpw.print("<uninitialized>");
1093         } else {
1094             TimeUtils.formatDuration(totalElapsedTimeMS - lastActionTimeMs, idpw);
1095         }
1096     }
1097 
dumpBucketExpiryTimes(IndentingPrintWriter idpw, AppUsageHistory appUsageHistory, long totalElapsedTimeMs)1098     private void dumpBucketExpiryTimes(IndentingPrintWriter idpw, AppUsageHistory appUsageHistory,
1099             long totalElapsedTimeMs) {
1100         idpw.print(" expiryTimes=");
1101         if (appUsageHistory.bucketExpiryTimesMs == null
1102                 || appUsageHistory.bucketExpiryTimesMs.size() == 0) {
1103             idpw.print("<none>");
1104             return;
1105         }
1106         idpw.print("(");
1107         final int size = appUsageHistory.bucketExpiryTimesMs.size();
1108         for (int i = 0; i < size; ++i) {
1109             final int bucket = appUsageHistory.bucketExpiryTimesMs.keyAt(i);
1110             final long expiryTimeMs = appUsageHistory.bucketExpiryTimesMs.valueAt(i);
1111             if (i != 0) {
1112                 idpw.print(",");
1113             }
1114             idpw.print(bucket + ":");
1115             TimeUtils.formatDuration(totalElapsedTimeMs - expiryTimeMs, idpw);
1116         }
1117         idpw.print(")");
1118     }
1119 }
1120