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