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