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_USAGE;
24 import static android.app.usage.UsageStatsManager.REASON_SUB_MASK;
25 import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION;
26 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
27 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
28 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
29 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
30 import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
31 
32 import static com.android.server.usage.AppStandbyController.isUserUsage;
33 
34 import android.app.usage.AppStandbyInfo;
35 import android.app.usage.UsageStatsManager;
36 import android.os.SystemClock;
37 import android.util.ArrayMap;
38 import android.util.AtomicFile;
39 import android.util.Slog;
40 import android.util.SparseArray;
41 import android.util.TimeUtils;
42 import android.util.Xml;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.util.CollectionUtils;
46 import com.android.internal.util.FastXmlSerializer;
47 import com.android.internal.util.FrameworkStatsLog;
48 import com.android.internal.util.IndentingPrintWriter;
49 
50 import libcore.io.IoUtils;
51 
52 import org.xmlpull.v1.XmlPullParser;
53 import org.xmlpull.v1.XmlPullParserException;
54 
55 import java.io.BufferedOutputStream;
56 import java.io.BufferedReader;
57 import java.io.File;
58 import java.io.FileInputStream;
59 import java.io.FileOutputStream;
60 import java.io.FileReader;
61 import java.io.IOException;
62 import java.nio.charset.StandardCharsets;
63 import java.util.ArrayList;
64 import java.util.List;
65 
66 /**
67  * Keeps track of recent active state changes in apps.
68  * Access should be guarded by a lock by the caller.
69  */
70 public class AppIdleHistory {
71 
72     private static final String TAG = "AppIdleHistory";
73 
74     private static final boolean DEBUG = AppStandbyController.DEBUG;
75 
76     // History for all users and all packages
77     private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>();
78     private static final long ONE_MINUTE = 60 * 1000;
79 
80     private static final int STANDBY_BUCKET_UNKNOWN = -1;
81 
82     /**
83      * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are
84      * considered idle while those in higher buckets are not considered idle.
85      */
86     static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE;
87 
88     @VisibleForTesting
89     static final String APP_IDLE_FILENAME = "app_idle_stats.xml";
90     private static final String TAG_PACKAGES = "packages";
91     private static final String TAG_PACKAGE = "package";
92     private static final String ATTR_NAME = "name";
93     // Screen on timebase time when app was last used
94     private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
95     // Elapsed timebase time when app was last used
96     private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
97     // Elapsed timebase time when app was last used by the user
98     private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime";
99     // Elapsed timebase time when the app bucket was last predicted externally
100     private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime";
101     // The standby bucket for the app
102     private static final String ATTR_CURRENT_BUCKET = "appLimitBucket";
103     // The reason the app was put in the above bucket
104     private static final String ATTR_BUCKETING_REASON = "bucketReason";
105     // The last time a job was run for this app
106     private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime";
107     // The time when the forced active state can be overridden.
108     private static final String ATTR_BUCKET_ACTIVE_TIMEOUT_TIME = "activeTimeoutTime";
109     // The time when the forced working_set state can be overridden.
110     private static final String ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME = "workingSetTimeoutTime";
111     // Elapsed timebase time when the app was last marked for restriction.
112     private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED =
113             "lastRestrictionAttemptElapsedTime";
114     // Reason why the app was last marked for restriction.
115     private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON =
116             "lastRestrictionAttemptReason";
117 
118     // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
119     private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
120     private long mElapsedDuration; // Total device on duration since device was "born"
121 
122     // screen on time = mScreenOnDuration + (timeNow - mScreenOnSnapshot)
123     private long mScreenOnSnapshot; // Elapsed time snapshot when last write of mScreenOnDuration
124     private long mScreenOnDuration; // Total screen on duration since device was "born"
125 
126     private final File mStorageDir;
127 
128     private boolean mScreenOn;
129 
130     static class AppUsageHistory {
131         // Last used time (including system usage), using elapsed timebase
132         long lastUsedElapsedTime;
133         // Last time the user used the app, using elapsed timebase
134         long lastUsedByUserElapsedTime;
135         // Last used time using screen_on timebase
136         long lastUsedScreenTime;
137         // Last predicted time using elapsed timebase
138         long lastPredictedTime;
139         // Last predicted bucket
140         @UsageStatsManager.StandbyBuckets
141         int lastPredictedBucket = STANDBY_BUCKET_UNKNOWN;
142         // Standby bucket
143         @UsageStatsManager.StandbyBuckets
144         int currentBucket;
145         // Reason for setting the standby bucket. The value here is a combination of
146         // one of UsageStatsManager.REASON_MAIN_* and one (or none) of
147         // UsageStatsManager.REASON_SUB_*. Also see REASON_MAIN_MASK and REASON_SUB_MASK.
148         int bucketingReason;
149         // In-memory only, last bucket for which the listeners were informed
150         int lastInformedBucket;
151         // The last time a job was run for this app, using elapsed timebase
152         long lastJobRunTime;
153         // When should the bucket active state timeout, in elapsed timebase, if greater than
154         // lastUsedElapsedTime.
155         // This is used to keep the app in a high bucket regardless of other timeouts and
156         // predictions.
157         long bucketActiveTimeoutTime;
158         // If there's a forced working_set state, this is when it times out. This can be sitting
159         // under any active state timeout, so that it becomes applicable after the active state
160         // timeout expires.
161         long bucketWorkingSetTimeoutTime;
162         // The last time an agent attempted to put the app into the RESTRICTED bucket.
163         long lastRestrictAttemptElapsedTime;
164         // The last reason the app was marked to be put into the RESTRICTED bucket.
165         int lastRestrictReason;
166     }
167 
AppIdleHistory(File storageDir, long elapsedRealtime)168     AppIdleHistory(File storageDir, long elapsedRealtime) {
169         mElapsedSnapshot = elapsedRealtime;
170         mScreenOnSnapshot = elapsedRealtime;
171         mStorageDir = storageDir;
172         readScreenOnTime();
173     }
174 
updateDisplay(boolean screenOn, long elapsedRealtime)175     public void updateDisplay(boolean screenOn, long elapsedRealtime) {
176         if (screenOn == mScreenOn) return;
177 
178         mScreenOn = screenOn;
179         if (mScreenOn) {
180             mScreenOnSnapshot = elapsedRealtime;
181         } else {
182             mScreenOnDuration += elapsedRealtime - mScreenOnSnapshot;
183             mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
184             mElapsedSnapshot = elapsedRealtime;
185         }
186         if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot
187                 + ", mScreenOnDuration=" + mScreenOnDuration
188                 + ", mScreenOn=" + mScreenOn);
189     }
190 
getScreenOnTime(long elapsedRealtime)191     public long getScreenOnTime(long elapsedRealtime) {
192         long screenOnTime = mScreenOnDuration;
193         if (mScreenOn) {
194             screenOnTime += elapsedRealtime - mScreenOnSnapshot;
195         }
196         return screenOnTime;
197     }
198 
199     @VisibleForTesting
getScreenOnTimeFile()200     File getScreenOnTimeFile() {
201         return new File(mStorageDir, "screen_on_time");
202     }
203 
readScreenOnTime()204     private void readScreenOnTime() {
205         File screenOnTimeFile = getScreenOnTimeFile();
206         if (screenOnTimeFile.exists()) {
207             try {
208                 BufferedReader reader = new BufferedReader(new FileReader(screenOnTimeFile));
209                 mScreenOnDuration = Long.parseLong(reader.readLine());
210                 mElapsedDuration = Long.parseLong(reader.readLine());
211                 reader.close();
212             } catch (IOException | NumberFormatException e) {
213             }
214         } else {
215             writeScreenOnTime();
216         }
217     }
218 
writeScreenOnTime()219     private void writeScreenOnTime() {
220         AtomicFile screenOnTimeFile = new AtomicFile(getScreenOnTimeFile());
221         FileOutputStream fos = null;
222         try {
223             fos = screenOnTimeFile.startWrite();
224             fos.write((Long.toString(mScreenOnDuration) + "\n"
225                     + Long.toString(mElapsedDuration) + "\n").getBytes());
226             screenOnTimeFile.finishWrite(fos);
227         } catch (IOException ioe) {
228             screenOnTimeFile.failWrite(fos);
229         }
230     }
231 
232     /**
233      * To be called periodically to keep track of elapsed time when app idle times are written
234      */
writeAppIdleDurations()235     public void writeAppIdleDurations() {
236         final long elapsedRealtime = SystemClock.elapsedRealtime();
237         // Only bump up and snapshot the elapsed time. Don't change screen on duration.
238         mElapsedDuration += elapsedRealtime - mElapsedSnapshot;
239         mElapsedSnapshot = elapsedRealtime;
240         writeScreenOnTime();
241     }
242 
243     /**
244      * Mark the app as used and update the bucket if necessary. If there is a timeout specified
245      * that's in the future, then the usage event is temporary and keeps the app in the specified
246      * bucket at least until the timeout is reached. This can be used to keep the app in an
247      * elevated bucket for a while until some important task gets to run.
248      * @param appUsageHistory the usage record for the app being updated
249      * @param packageName name of the app being updated, for logging purposes
250      * @param newBucket the bucket to set the app to
251      * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_*
252      * @param elapsedRealtime mark as used time if non-zero
253      * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used
254      *                with bucket values of ACTIVE and WORKING_SET.
255      * @return {@code appUsageHistory}
256      */
reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, int newBucket, int usageReason, long elapsedRealtime, long timeout)257     AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId,
258             int newBucket, int usageReason, long elapsedRealtime, long timeout) {
259         int bucketingReason = REASON_MAIN_USAGE | usageReason;
260         final boolean isUserUsage = isUserUsage(bucketingReason);
261 
262         if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage) {
263             // Only user usage should bring an app out of the RESTRICTED bucket.
264             newBucket = STANDBY_BUCKET_RESTRICTED;
265             bucketingReason = appUsageHistory.bucketingReason;
266         } else {
267             // Set the timeout if applicable
268             if (timeout > elapsedRealtime) {
269                 // Convert to elapsed timebase
270                 final long timeoutTime = mElapsedDuration + (timeout - mElapsedSnapshot);
271                 if (newBucket == STANDBY_BUCKET_ACTIVE) {
272                     appUsageHistory.bucketActiveTimeoutTime = Math.max(timeoutTime,
273                             appUsageHistory.bucketActiveTimeoutTime);
274                 } else if (newBucket == STANDBY_BUCKET_WORKING_SET) {
275                     appUsageHistory.bucketWorkingSetTimeoutTime = Math.max(timeoutTime,
276                             appUsageHistory.bucketWorkingSetTimeoutTime);
277                 } else {
278                     throw new IllegalArgumentException("Cannot set a timeout on bucket="
279                             + newBucket);
280                 }
281             }
282         }
283 
284         if (elapsedRealtime != 0) {
285             appUsageHistory.lastUsedElapsedTime = mElapsedDuration
286                     + (elapsedRealtime - mElapsedSnapshot);
287             if (isUserUsage) {
288                 appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime;
289             }
290             appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
291         }
292 
293         if (appUsageHistory.currentBucket > newBucket) {
294             appUsageHistory.currentBucket = newBucket;
295             logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason);
296         }
297         appUsageHistory.bucketingReason = bucketingReason;
298 
299         return appUsageHistory;
300     }
301 
302     /**
303      * Mark the app as used and update the bucket if necessary. If there is a timeout specified
304      * that's in the future, then the usage event is temporary and keeps the app in the specified
305      * bucket at least until the timeout is reached. This can be used to keep the app in an
306      * elevated bucket for a while until some important task gets to run.
307      * @param packageName
308      * @param userId
309      * @param newBucket the bucket to set the app to
310      * @param usageReason sub reason for usage
311      * @param nowElapsed mark as used time if non-zero
312      * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used
313      *                with bucket values of ACTIVE and WORKING_SET.
314      * @return
315      */
reportUsage(String packageName, int userId, int newBucket, int usageReason, long nowElapsed, long timeout)316     public AppUsageHistory reportUsage(String packageName, int userId, int newBucket,
317             int usageReason, long nowElapsed, long timeout) {
318         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
319         AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true);
320         return reportUsage(history, packageName, userId, newBucket, usageReason, nowElapsed,
321                 timeout);
322     }
323 
getUserHistory(int userId)324     private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) {
325         ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
326         if (userHistory == null) {
327             userHistory = new ArrayMap<>();
328             mIdleHistory.put(userId, userHistory);
329             readAppIdleTimes(userId, userHistory);
330         }
331         return userHistory;
332     }
333 
getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory, String packageName, long elapsedRealtime, boolean create)334     private AppUsageHistory getPackageHistory(ArrayMap<String, AppUsageHistory> userHistory,
335             String packageName, long elapsedRealtime, boolean create) {
336         AppUsageHistory appUsageHistory = userHistory.get(packageName);
337         if (appUsageHistory == null && create) {
338             appUsageHistory = new AppUsageHistory();
339             appUsageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime);
340             appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
341             appUsageHistory.lastPredictedTime = getElapsedTime(0);
342             appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER;
343             appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT;
344             appUsageHistory.lastInformedBucket = -1;
345             appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago
346             userHistory.put(packageName, appUsageHistory);
347         }
348         return appUsageHistory;
349     }
350 
onUserRemoved(int userId)351     public void onUserRemoved(int userId) {
352         mIdleHistory.remove(userId);
353     }
354 
isIdle(String packageName, int userId, long elapsedRealtime)355     public boolean isIdle(String packageName, int userId, long elapsedRealtime) {
356         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
357         AppUsageHistory appUsageHistory =
358                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
359         return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF;
360     }
361 
getAppUsageHistory(String packageName, int userId, long elapsedRealtime)362     public AppUsageHistory getAppUsageHistory(String packageName, int userId,
363             long elapsedRealtime) {
364         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
365         AppUsageHistory appUsageHistory =
366                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
367         return appUsageHistory;
368     }
369 
setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason)370     public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
371             int bucket, int reason) {
372         setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false);
373     }
374 
setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, int bucket, int reason, boolean resetTimeout)375     public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
376             int bucket, int reason, boolean resetTimeout) {
377         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
378         AppUsageHistory appUsageHistory =
379                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
380         final boolean changed = appUsageHistory.currentBucket != bucket;
381         appUsageHistory.currentBucket = bucket;
382         appUsageHistory.bucketingReason = reason;
383 
384         final long elapsed = getElapsedTime(elapsedRealtime);
385 
386         if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) {
387             appUsageHistory.lastPredictedTime = elapsed;
388             appUsageHistory.lastPredictedBucket = bucket;
389         }
390         if (resetTimeout) {
391             appUsageHistory.bucketActiveTimeoutTime = elapsed;
392             appUsageHistory.bucketWorkingSetTimeoutTime = elapsed;
393         }
394         if (changed) {
395             logAppStandbyBucketChanged(packageName, userId, bucket, reason);
396         }
397     }
398 
399     /**
400      * Update the prediction for the app but don't change the actual bucket
401      * @param app The app for which the prediction was made
402      * @param elapsedTimeAdjusted The elapsed time in the elapsed duration timebase
403      * @param bucket The predicted bucket
404      */
updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket)405     public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) {
406         app.lastPredictedTime = elapsedTimeAdjusted;
407         app.lastPredictedBucket = bucket;
408     }
409 
410     /**
411      * Marks the last time a job was run, with the given elapsedRealtime. The time stored is
412      * based on the elapsed timebase.
413      * @param packageName
414      * @param userId
415      * @param elapsedRealtime
416      */
setLastJobRunTime(String packageName, int userId, long elapsedRealtime)417     public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
418         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
419         AppUsageHistory appUsageHistory =
420                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
421         appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime);
422     }
423 
424     /**
425      * Notes an attempt to put the app in the {@link UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
426      * bucket.
427      *
428      * @param packageName     The package name of the app that is being restricted
429      * @param userId          The ID of the user in which the app is being restricted
430      * @param elapsedRealtime The time the attempt was made, in the (unadjusted) elapsed realtime
431      *                        timebase
432      * @param reason          The reason for the restriction attempt
433      */
noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason)434     void noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason) {
435         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
436         AppUsageHistory appUsageHistory =
437                 getPackageHistory(userHistory, packageName, elapsedRealtime, true);
438         appUsageHistory.lastRestrictAttemptElapsedTime = getElapsedTime(elapsedRealtime);
439         appUsageHistory.lastRestrictReason = reason;
440     }
441 
442     /**
443      * Returns the time since the last job was run for this app. This can be larger than the
444      * current elapsedRealtime, in case it happened before boot or a really large value if no jobs
445      * were ever run.
446      * @param packageName
447      * @param userId
448      * @param elapsedRealtime
449      * @return
450      */
getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime)451     public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) {
452         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
453         AppUsageHistory appUsageHistory =
454                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
455         // Don't adjust the default, else it'll wrap around to a positive value
456         if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) {
457             return Long.MAX_VALUE;
458         }
459         return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime;
460     }
461 
getAppStandbyBucket(String packageName, int userId, long elapsedRealtime)462     public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) {
463         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
464         AppUsageHistory appUsageHistory =
465                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
466         return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket;
467     }
468 
getAppStandbyBuckets(int userId, boolean appIdleEnabled)469     public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) {
470         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
471         int size = userHistory.size();
472         ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size);
473         for (int i = 0; i < size; i++) {
474             buckets.add(new AppStandbyInfo(userHistory.keyAt(i),
475                     appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE));
476         }
477         return buckets;
478     }
479 
getAppStandbyReason(String packageName, int userId, long elapsedRealtime)480     public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) {
481         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
482         AppUsageHistory appUsageHistory =
483                 getPackageHistory(userHistory, packageName, elapsedRealtime, false);
484         return appUsageHistory != null ? appUsageHistory.bucketingReason : 0;
485     }
486 
getElapsedTime(long elapsedRealtime)487     public long getElapsedTime(long elapsedRealtime) {
488         return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
489     }
490 
491     /* Returns the new standby bucket the app is assigned to */
setIdle(String packageName, int userId, boolean idle, long elapsedRealtime)492     public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) {
493         final int newBucket;
494         final int reason;
495         if (idle) {
496             newBucket = IDLE_BUCKET_CUTOFF;
497             reason = REASON_MAIN_FORCED_BY_USER;
498         } else {
499             newBucket = STANDBY_BUCKET_ACTIVE;
500             // This is to pretend that the app was just used, don't freeze the state anymore.
501             reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION;
502         }
503         setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, false);
504 
505         return newBucket;
506     }
507 
clearUsage(String packageName, int userId)508     public void clearUsage(String packageName, int userId) {
509         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
510         userHistory.remove(packageName);
511     }
512 
shouldInformListeners(String packageName, int userId, long elapsedRealtime, int bucket)513     boolean shouldInformListeners(String packageName, int userId,
514             long elapsedRealtime, int bucket) {
515         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
516         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
517                 elapsedRealtime, true);
518         if (appUsageHistory.lastInformedBucket != bucket) {
519             appUsageHistory.lastInformedBucket = bucket;
520             return true;
521         }
522         return false;
523     }
524 
525     /**
526      * Returns the index in the arrays of screenTimeThresholds and elapsedTimeThresholds
527      * that corresponds to how long since the app was used.
528      * @param packageName
529      * @param userId
530      * @param elapsedRealtime current time
531      * @param screenTimeThresholds Array of screen times, in ascending order, first one is 0
532      * @param elapsedTimeThresholds Array of elapsed time, in ascending order, first one is 0
533      * @return The index whose values the app's used time exceeds (in both arrays)
534      */
getThresholdIndex(String packageName, int userId, long elapsedRealtime, long[] screenTimeThresholds, long[] elapsedTimeThresholds)535     int getThresholdIndex(String packageName, int userId, long elapsedRealtime,
536             long[] screenTimeThresholds, long[] elapsedTimeThresholds) {
537         ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
538         AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
539                 elapsedRealtime, false);
540         // If we don't have any state for the app, assume never used
541         if (appUsageHistory == null) return screenTimeThresholds.length - 1;
542 
543         long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
544         long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
545 
546         if (DEBUG) Slog.d(TAG, packageName
547                 + " lastUsedScreen=" + appUsageHistory.lastUsedScreenTime
548                 + " lastUsedElapsed=" + appUsageHistory.lastUsedElapsedTime);
549         if (DEBUG) Slog.d(TAG, packageName + " screenOn=" + screenOnDelta
550                 + ", elapsed=" + elapsedDelta);
551         for (int i = screenTimeThresholds.length - 1; i >= 0; i--) {
552             if (screenOnDelta >= screenTimeThresholds[i]
553                 && elapsedDelta >= elapsedTimeThresholds[i]) {
554                 return i;
555             }
556         }
557         return 0;
558     }
559 
560     /**
561      * Log a standby bucket change to statsd, and also logcat if debug logging is enabled.
562      */
logAppStandbyBucketChanged(String packageName, int userId, int bucket, int reason)563     private void logAppStandbyBucketChanged(String packageName, int userId, int bucket,
564             int reason) {
565         FrameworkStatsLog.write(
566                 FrameworkStatsLog.APP_STANDBY_BUCKET_CHANGED,
567                 packageName, userId, bucket,
568                 (reason & REASON_MAIN_MASK), (reason & REASON_SUB_MASK));
569         if (DEBUG) {
570             Slog.d(TAG, "Moved " + packageName + " to bucket=" + bucket
571                     + ", reason=0x0" + Integer.toHexString(reason));
572         }
573     }
574 
575     @VisibleForTesting
getUserFile(int userId)576     File getUserFile(int userId) {
577         return new File(new File(new File(mStorageDir, "users"),
578                 Integer.toString(userId)), APP_IDLE_FILENAME);
579     }
580 
581     /**
582      * Check if App Idle File exists on disk
583      * @param userId
584      * @return true if file exists
585      */
userFileExists(int userId)586     public boolean userFileExists(int userId) {
587         return getUserFile(userId).exists();
588     }
589 
readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory)590     private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) {
591         FileInputStream fis = null;
592         try {
593             AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
594             fis = appIdleFile.openRead();
595             XmlPullParser parser = Xml.newPullParser();
596             parser.setInput(fis, StandardCharsets.UTF_8.name());
597 
598             int type;
599             while ((type = parser.next()) != XmlPullParser.START_TAG
600                     && type != XmlPullParser.END_DOCUMENT) {
601                 // Skip
602             }
603 
604             if (type != XmlPullParser.START_TAG) {
605                 Slog.e(TAG, "Unable to read app idle file for user " + userId);
606                 return;
607             }
608             if (!parser.getName().equals(TAG_PACKAGES)) {
609                 return;
610             }
611             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
612                 if (type == XmlPullParser.START_TAG) {
613                     final String name = parser.getName();
614                     if (name.equals(TAG_PACKAGE)) {
615                         final String packageName = parser.getAttributeValue(null, ATTR_NAME);
616                         AppUsageHistory appUsageHistory = new AppUsageHistory();
617                         appUsageHistory.lastUsedElapsedTime =
618                                 Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
619                         appUsageHistory.lastUsedByUserElapsedTime = getLongValue(parser,
620                                 ATTR_LAST_USED_BY_USER_ELAPSED,
621                                 appUsageHistory.lastUsedElapsedTime);
622                         appUsageHistory.lastUsedScreenTime =
623                                 Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
624                         appUsageHistory.lastPredictedTime = getLongValue(parser,
625                                 ATTR_LAST_PREDICTED_TIME, 0L);
626                         String currentBucketString = parser.getAttributeValue(null,
627                                 ATTR_CURRENT_BUCKET);
628                         appUsageHistory.currentBucket = currentBucketString == null
629                                 ? STANDBY_BUCKET_ACTIVE
630                                 : Integer.parseInt(currentBucketString);
631                         String bucketingReason =
632                                 parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
633                         appUsageHistory.lastJobRunTime = getLongValue(parser,
634                                 ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE);
635                         appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser,
636                                 ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L);
637                         appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser,
638                                 ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L);
639                         appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT;
640                         if (bucketingReason != null) {
641                             try {
642                                 appUsageHistory.bucketingReason =
643                                         Integer.parseInt(bucketingReason, 16);
644                             } catch (NumberFormatException nfe) {
645                                 Slog.wtf(TAG, "Unable to read bucketing reason", nfe);
646                             }
647                         }
648                         appUsageHistory.lastRestrictAttemptElapsedTime =
649                                 getLongValue(parser, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 0);
650                         String lastRestrictReason = parser.getAttributeValue(
651                                 null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON);
652                         if (lastRestrictReason != null) {
653                             try {
654                                 appUsageHistory.lastRestrictReason =
655                                         Integer.parseInt(lastRestrictReason, 16);
656                             } catch (NumberFormatException nfe) {
657                                 Slog.wtf(TAG, "Unable to read last restrict reason", nfe);
658                             }
659                         }
660                         appUsageHistory.lastInformedBucket = -1;
661                         userHistory.put(packageName, appUsageHistory);
662                     }
663                 }
664             }
665         } catch (IOException | XmlPullParserException e) {
666             Slog.e(TAG, "Unable to read app idle file for user " + userId, e);
667         } finally {
668             IoUtils.closeQuietly(fis);
669         }
670     }
671 
getLongValue(XmlPullParser parser, String attrName, long defValue)672     private long getLongValue(XmlPullParser parser, String attrName, long defValue) {
673         String value = parser.getAttributeValue(null, attrName);
674         if (value == null) return defValue;
675         return Long.parseLong(value);
676     }
677 
678 
writeAppIdleTimes()679     public void writeAppIdleTimes() {
680         final int size = mIdleHistory.size();
681         for (int i = 0; i < size; i++) {
682             writeAppIdleTimes(mIdleHistory.keyAt(i));
683         }
684     }
685 
writeAppIdleTimes(int userId)686     public void writeAppIdleTimes(int userId) {
687         FileOutputStream fos = null;
688         AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
689         try {
690             fos = appIdleFile.startWrite();
691             final BufferedOutputStream bos = new BufferedOutputStream(fos);
692 
693             FastXmlSerializer xml = new FastXmlSerializer();
694             xml.setOutput(bos, StandardCharsets.UTF_8.name());
695             xml.startDocument(null, true);
696             xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
697 
698             xml.startTag(null, TAG_PACKAGES);
699 
700             ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId);
701             final int N = userHistory.size();
702             for (int i = 0; i < N; i++) {
703                 String packageName = userHistory.keyAt(i);
704                 // Skip any unexpected null package names
705                 if (packageName == null) {
706                     Slog.w(TAG, "Skipping App Idle write for unexpected null package");
707                     continue;
708                 }
709                 AppUsageHistory history = userHistory.valueAt(i);
710                 xml.startTag(null, TAG_PACKAGE);
711                 xml.attribute(null, ATTR_NAME, packageName);
712                 xml.attribute(null, ATTR_ELAPSED_IDLE,
713                         Long.toString(history.lastUsedElapsedTime));
714                 xml.attribute(null, ATTR_LAST_USED_BY_USER_ELAPSED,
715                         Long.toString(history.lastUsedByUserElapsedTime));
716                 xml.attribute(null, ATTR_SCREEN_IDLE,
717                         Long.toString(history.lastUsedScreenTime));
718                 xml.attribute(null, ATTR_LAST_PREDICTED_TIME,
719                         Long.toString(history.lastPredictedTime));
720                 xml.attribute(null, ATTR_CURRENT_BUCKET,
721                         Integer.toString(history.currentBucket));
722                 xml.attribute(null, ATTR_BUCKETING_REASON,
723                         Integer.toHexString(history.bucketingReason));
724                 if (history.bucketActiveTimeoutTime > 0) {
725                     xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history
726                             .bucketActiveTimeoutTime));
727                 }
728                 if (history.bucketWorkingSetTimeoutTime > 0) {
729                     xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history
730                             .bucketWorkingSetTimeoutTime));
731                 }
732                 if (history.lastJobRunTime != Long.MIN_VALUE) {
733                     xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history
734                             .lastJobRunTime));
735                 }
736                 if (history.lastRestrictAttemptElapsedTime > 0) {
737                     xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED,
738                             Long.toString(history.lastRestrictAttemptElapsedTime));
739                 }
740                 xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON,
741                         Integer.toHexString(history.lastRestrictReason));
742                 xml.endTag(null, TAG_PACKAGE);
743             }
744 
745             xml.endTag(null, TAG_PACKAGES);
746             xml.endDocument();
747             appIdleFile.finishWrite(fos);
748         } catch (Exception e) {
749             appIdleFile.failWrite(fos);
750             Slog.e(TAG, "Error writing app idle file for user " + userId, e);
751         }
752     }
753 
dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs)754     public void dumpUsers(IndentingPrintWriter idpw, int[] userIds, List<String> pkgs) {
755         final int numUsers = userIds.length;
756         for (int i = 0; i < numUsers; i++) {
757             idpw.println();
758             dumpUser(idpw, userIds[i], pkgs);
759         }
760     }
761 
dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs)762     private void dumpUser(IndentingPrintWriter idpw, int userId, List<String> pkgs) {
763         idpw.print("User ");
764         idpw.print(userId);
765         idpw.println(" App Standby States:");
766         idpw.increaseIndent();
767         ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
768         final long elapsedRealtime = SystemClock.elapsedRealtime();
769         final long totalElapsedTime = getElapsedTime(elapsedRealtime);
770         final long screenOnTime = getScreenOnTime(elapsedRealtime);
771         if (userHistory == null) return;
772         final int P = userHistory.size();
773         for (int p = 0; p < P; p++) {
774             final String packageName = userHistory.keyAt(p);
775             final AppUsageHistory appUsageHistory = userHistory.valueAt(p);
776             if (!CollectionUtils.isEmpty(pkgs) && !pkgs.contains(packageName)) {
777                 continue;
778             }
779             idpw.print("package=" + packageName);
780             idpw.print(" u=" + userId);
781             idpw.print(" bucket=" + appUsageHistory.currentBucket
782                     + " reason="
783                     + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason));
784             idpw.print(" used=");
785             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw);
786             idpw.print(" usedByUser=");
787             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedByUserElapsedTime,
788                     idpw);
789             idpw.print(" usedScr=");
790             TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
791             idpw.print(" lastPred=");
792             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw);
793             idpw.print(" activeLeft=");
794             TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime,
795                     idpw);
796             idpw.print(" wsLeft=");
797             TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime,
798                     idpw);
799             idpw.print(" lastJob=");
800             TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
801             if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) {
802                 idpw.print(" lastRestrictAttempt=");
803                 TimeUtils.formatDuration(
804                         totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw);
805                 idpw.print(" lastRestrictReason="
806                         + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason));
807             }
808             idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
809             idpw.println();
810         }
811         idpw.println();
812         idpw.print("totalElapsedTime=");
813         TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw);
814         idpw.println();
815         idpw.print("totalScreenOnTime=");
816         TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw);
817         idpw.println();
818         idpw.decreaseIndent();
819     }
820 }
821