Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 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 | |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 19 | import static android.app.usage.UsageStatsManager.REASON_MAIN_DEFAULT; |
Kweku Adams | c182d5e | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 20 | import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER; |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 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; |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 24 | import static android.app.usage.UsageStatsManager.REASON_SUB_MASK; |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 25 | import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION; |
Amith Yamasani | 172612c | 2017-12-15 10:51:53 -0800 | [diff] [blame] | 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; |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 29 | import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 30 | import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; |
Amith Yamasani | 172612c | 2017-12-15 10:51:53 -0800 | [diff] [blame] | 31 | |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 32 | import static com.android.server.usage.AppStandbyController.isUserUsage; |
| 33 | |
Suprabh Shukla | 868bde2 | 2018-02-20 20:59:52 -0800 | [diff] [blame] | 34 | import android.app.usage.AppStandbyInfo; |
Amith Yamasani | afbccb7 | 2017-11-27 10:44:24 -0800 | [diff] [blame] | 35 | import android.app.usage.UsageStatsManager; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 36 | import android.os.SystemClock; |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 37 | import android.util.ArrayMap; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 38 | import android.util.AtomicFile; |
| 39 | import android.util.Slog; |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 40 | import android.util.SparseArray; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 41 | import android.util.TimeUtils; |
| 42 | import android.util.Xml; |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 43 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 44 | import com.android.internal.annotations.VisibleForTesting; |
Sudheer Shanka | b497dd4 | 2020-02-16 22:23:24 -0800 | [diff] [blame] | 45 | import com.android.internal.util.CollectionUtils; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 46 | import com.android.internal.util.FastXmlSerializer; |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 47 | import com.android.internal.util.FrameworkStatsLog; |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 48 | import com.android.internal.util.IndentingPrintWriter; |
| 49 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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; |
Suprabh Shukla | 868bde2 | 2018-02-20 20:59:52 -0800 | [diff] [blame] | 63 | import java.util.ArrayList; |
Sudheer Shanka | b497dd4 | 2020-02-16 22:23:24 -0800 | [diff] [blame] | 64 | import java.util.List; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 65 | |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 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 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 72 | private static final String TAG = "AppIdleHistory"; |
| 73 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 74 | private static final boolean DEBUG = AppStandbyController.DEBUG; |
| 75 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 76 | // History for all users and all packages |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 77 | private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>(); |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 78 | private static final long ONE_MINUTE = 60 * 1000; |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 79 | |
Amith Yamasani | 3154dcf | 2018-03-27 18:24:04 -0700 | [diff] [blame] | 80 | private static final int STANDBY_BUCKET_UNKNOWN = -1; |
| 81 | |
Kweku Adams | 95cd952 | 2020-05-08 09:56:53 -0700 | [diff] [blame] | 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 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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"; |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 97 | // Elapsed timebase time when app was last used by the user |
| 98 | private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime"; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 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 |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 102 | private static final String ATTR_CURRENT_BUCKET = "appLimitBucket"; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 103 | // The reason the app was put in the above bucket |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 104 | private static final String ATTR_BUCKETING_REASON = "bucketReason"; |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 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. |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 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"; |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 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"; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 117 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 126 | private final File mStorageDir; |
| 127 | |
| 128 | private boolean mScreenOn; |
| 129 | |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 130 | static class AppUsageHistory { |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 131 | // Last used time (including system usage), using elapsed timebase |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 132 | long lastUsedElapsedTime; |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 133 | // Last time the user used the app, using elapsed timebase |
| 134 | long lastUsedByUserElapsedTime; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 135 | // Last used time using screen_on timebase |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 136 | long lastUsedScreenTime; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 137 | // Last predicted time using elapsed timebase |
| 138 | long lastPredictedTime; |
Amith Yamasani | 3154dcf | 2018-03-27 18:24:04 -0700 | [diff] [blame] | 139 | // Last predicted bucket |
| 140 | @UsageStatsManager.StandbyBuckets |
| 141 | int lastPredictedBucket = STANDBY_BUCKET_UNKNOWN; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 142 | // Standby bucket |
Amith Yamasani | afbccb7 | 2017-11-27 10:44:24 -0800 | [diff] [blame] | 143 | @UsageStatsManager.StandbyBuckets |
| 144 | int currentBucket; |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 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; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 149 | // In-memory only, last bucket for which the listeners were informed |
Amith Yamasani | 84cd7b7 | 2017-11-07 13:59:37 -0800 | [diff] [blame] | 150 | int lastInformedBucket; |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 151 | // The last time a job was run for this app, using elapsed timebase |
| 152 | long lastJobRunTime; |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 153 | // When should the bucket active state timeout, in elapsed timebase, if greater than |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 154 | // lastUsedElapsedTime. |
| 155 | // This is used to keep the app in a high bucket regardless of other timeouts and |
| 156 | // predictions. |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 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; |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 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; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 166 | } |
| 167 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 168 | AppIdleHistory(File storageDir, long elapsedRealtime) { |
| 169 | mElapsedSnapshot = elapsedRealtime; |
| 170 | mScreenOnSnapshot = elapsedRealtime; |
| 171 | mStorageDir = storageDir; |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 172 | readScreenOnTime(); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 173 | } |
| 174 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 175 | public void updateDisplay(boolean screenOn, long elapsedRealtime) { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 184 | mElapsedSnapshot = elapsedRealtime; |
| 185 | } |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 186 | if (DEBUG) Slog.d(TAG, "mScreenOnSnapshot=" + mScreenOnSnapshot |
| 187 | + ", mScreenOnDuration=" + mScreenOnDuration |
| 188 | + ", mScreenOn=" + mScreenOn); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 189 | } |
| 190 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 191 | public long getScreenOnTime(long elapsedRealtime) { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 192 | long screenOnTime = mScreenOnDuration; |
| 193 | if (mScreenOn) { |
| 194 | screenOnTime += elapsedRealtime - mScreenOnSnapshot; |
| 195 | } |
| 196 | return screenOnTime; |
| 197 | } |
| 198 | |
| 199 | @VisibleForTesting |
| 200 | File getScreenOnTimeFile() { |
| 201 | return new File(mStorageDir, "screen_on_time"); |
| 202 | } |
| 203 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 204 | private void readScreenOnTime() { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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 { |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 215 | writeScreenOnTime(); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 216 | } |
| 217 | } |
| 218 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 219 | private void writeScreenOnTime() { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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 | */ |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 235 | public void writeAppIdleDurations() { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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; |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 240 | writeScreenOnTime(); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 241 | } |
| 242 | |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 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. |
Christopher Tate | d117b29 | 2018-01-05 17:32:36 -0800 | [diff] [blame] | 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 |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 251 | * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_* |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 252 | * @param elapsedRealtime mark as used time if non-zero |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 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. |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 255 | * @return {@code appUsageHistory} |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 256 | */ |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 257 | AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 258 | int newBucket, int usageReason, long elapsedRealtime, long timeout) { |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 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 | } |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 281 | } |
| 282 | } |
| 283 | |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 284 | if (elapsedRealtime != 0) { |
| 285 | appUsageHistory.lastUsedElapsedTime = mElapsedDuration |
| 286 | + (elapsedRealtime - mElapsedSnapshot); |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 287 | if (isUserUsage) { |
| 288 | appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime; |
| 289 | } |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 290 | appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); |
| 291 | } |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 292 | |
Christopher Tate | d117b29 | 2018-01-05 17:32:36 -0800 | [diff] [blame] | 293 | if (appUsageHistory.currentBucket > newBucket) { |
| 294 | appUsageHistory.currentBucket = newBucket; |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 295 | logAppStandbyBucketChanged(packageName, userId, newBucket, bucketingReason); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 296 | } |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 297 | appUsageHistory.bucketingReason = bucketingReason; |
Amith Yamasani | 803eab69 | 2017-11-09 17:47:04 -0800 | [diff] [blame] | 298 | |
Christopher Tate | d117b29 | 2018-01-05 17:32:36 -0800 | [diff] [blame] | 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 |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 310 | * @param usageReason sub reason for usage |
| 311 | * @param nowElapsed mark as used time if non-zero |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 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. |
Christopher Tate | d117b29 | 2018-01-05 17:32:36 -0800 | [diff] [blame] | 314 | * @return |
| 315 | */ |
| 316 | public AppUsageHistory reportUsage(String packageName, int userId, int newBucket, |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 317 | int usageReason, long nowElapsed, long timeout) { |
Christopher Tate | d117b29 | 2018-01-05 17:32:36 -0800 | [diff] [blame] | 318 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 319 | AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true); |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 320 | return reportUsage(history, packageName, userId, newBucket, usageReason, nowElapsed, |
| 321 | timeout); |
Amith Yamasani | 803eab69 | 2017-11-09 17:47:04 -0800 | [diff] [blame] | 322 | } |
| 323 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 324 | private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) { |
| 325 | ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); |
Amith Yamasani | 6776849 | 2015-06-09 12:23:58 -0700 | [diff] [blame] | 326 | if (userHistory == null) { |
| 327 | userHistory = new ArrayMap<>(); |
| 328 | mIdleHistory.put(userId, userHistory); |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 329 | readAppIdleTimes(userId, userHistory); |
Amith Yamasani | 6776849 | 2015-06-09 12:23:58 -0700 | [diff] [blame] | 330 | } |
| 331 | return userHistory; |
| 332 | } |
| 333 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 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); |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 341 | appUsageHistory.lastPredictedTime = getElapsedTime(0); |
Amith Yamasani | 172612c | 2017-12-15 10:51:53 -0800 | [diff] [blame] | 342 | appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER; |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 343 | appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; |
Amith Yamasani | afbccb7 | 2017-11-27 10:44:24 -0800 | [diff] [blame] | 344 | appUsageHistory.lastInformedBucket = -1; |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 345 | appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 346 | userHistory.put(packageName, appUsageHistory); |
Amith Yamasani | 6776849 | 2015-06-09 12:23:58 -0700 | [diff] [blame] | 347 | } |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 348 | return appUsageHistory; |
Amith Yamasani | 6776849 | 2015-06-09 12:23:58 -0700 | [diff] [blame] | 349 | } |
| 350 | |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 351 | public void onUserRemoved(int userId) { |
Amith Yamasani | 6776849 | 2015-06-09 12:23:58 -0700 | [diff] [blame] | 352 | mIdleHistory.remove(userId); |
| 353 | } |
| 354 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 355 | public boolean isIdle(String packageName, int userId, long elapsedRealtime) { |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 356 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 357 | AppUsageHistory appUsageHistory = |
| 358 | getPackageHistory(userHistory, packageName, elapsedRealtime, true); |
Kweku Adams | 95cd952 | 2020-05-08 09:56:53 -0700 | [diff] [blame] | 359 | return appUsageHistory.currentBucket >= IDLE_BUCKET_CUTOFF; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 360 | } |
| 361 | |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 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 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 370 | public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 371 | int bucket, int reason) { |
Makoto Onuki | a0058b4 | 2018-05-22 16:32:23 -0700 | [diff] [blame] | 372 | setAppStandbyBucket(packageName, userId, elapsedRealtime, bucket, reason, false); |
| 373 | } |
| 374 | |
| 375 | public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, |
| 376 | int bucket, int reason, boolean resetTimeout) { |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 377 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 378 | AppUsageHistory appUsageHistory = |
| 379 | getPackageHistory(userHistory, packageName, elapsedRealtime, true); |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 380 | final boolean changed = appUsageHistory.currentBucket != bucket; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 381 | appUsageHistory.currentBucket = bucket; |
| 382 | appUsageHistory.bucketingReason = reason; |
Makoto Onuki | a0058b4 | 2018-05-22 16:32:23 -0700 | [diff] [blame] | 383 | |
| 384 | final long elapsed = getElapsedTime(elapsedRealtime); |
| 385 | |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 386 | if ((reason & REASON_MAIN_MASK) == REASON_MAIN_PREDICTED) { |
Makoto Onuki | a0058b4 | 2018-05-22 16:32:23 -0700 | [diff] [blame] | 387 | appUsageHistory.lastPredictedTime = elapsed; |
Amith Yamasani | 3154dcf | 2018-03-27 18:24:04 -0700 | [diff] [blame] | 388 | appUsageHistory.lastPredictedBucket = bucket; |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 389 | } |
Makoto Onuki | a0058b4 | 2018-05-22 16:32:23 -0700 | [diff] [blame] | 390 | if (resetTimeout) { |
| 391 | appUsageHistory.bucketActiveTimeoutTime = elapsed; |
| 392 | appUsageHistory.bucketWorkingSetTimeoutTime = elapsed; |
| 393 | } |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 394 | if (changed) { |
| 395 | logAppStandbyBucketChanged(packageName, userId, bucket, reason); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 396 | } |
| 397 | } |
| 398 | |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 399 | /** |
Amith Yamasani | 3154dcf | 2018-03-27 18:24:04 -0700 | [diff] [blame] | 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 | */ |
| 405 | public void updateLastPrediction(AppUsageHistory app, long elapsedTimeAdjusted, int bucket) { |
| 406 | app.lastPredictedTime = elapsedTimeAdjusted; |
| 407 | app.lastPredictedBucket = bucket; |
| 408 | } |
| 409 | |
| 410 | /** |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 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 | */ |
| 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 | /** |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 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 | */ |
| 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 | /** |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 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 | */ |
| 451 | public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) { |
| 452 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 453 | AppUsageHistory appUsageHistory = |
Varun Shah | 7609b75 | 2018-10-15 15:07:47 -0700 | [diff] [blame] | 454 | getPackageHistory(userHistory, packageName, elapsedRealtime, false); |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 455 | // Don't adjust the default, else it'll wrap around to a positive value |
Varun Shah | 7609b75 | 2018-10-15 15:07:47 -0700 | [diff] [blame] | 456 | if (appUsageHistory == null || appUsageHistory.lastJobRunTime == Long.MIN_VALUE) { |
| 457 | return Long.MAX_VALUE; |
| 458 | } |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 459 | return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime; |
| 460 | } |
| 461 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 462 | public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) { |
| 463 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 464 | AppUsageHistory appUsageHistory = |
Varun Shah | 7609b75 | 2018-10-15 15:07:47 -0700 | [diff] [blame] | 465 | getPackageHistory(userHistory, packageName, elapsedRealtime, false); |
| 466 | return appUsageHistory == null ? STANDBY_BUCKET_NEVER : appUsageHistory.currentBucket; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 467 | } |
| 468 | |
Suprabh Shukla | 868bde2 | 2018-02-20 20:59:52 -0800 | [diff] [blame] | 469 | public ArrayList<AppStandbyInfo> getAppStandbyBuckets(int userId, boolean appIdleEnabled) { |
Amith Yamasani | e878931 | 2017-12-10 14:34:26 -0800 | [diff] [blame] | 470 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 471 | int size = userHistory.size(); |
Suprabh Shukla | 868bde2 | 2018-02-20 20:59:52 -0800 | [diff] [blame] | 472 | ArrayList<AppStandbyInfo> buckets = new ArrayList<>(size); |
Amith Yamasani | e878931 | 2017-12-10 14:34:26 -0800 | [diff] [blame] | 473 | for (int i = 0; i < size; i++) { |
Suprabh Shukla | 868bde2 | 2018-02-20 20:59:52 -0800 | [diff] [blame] | 474 | buckets.add(new AppStandbyInfo(userHistory.keyAt(i), |
| 475 | appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE)); |
Amith Yamasani | e878931 | 2017-12-10 14:34:26 -0800 | [diff] [blame] | 476 | } |
| 477 | return buckets; |
| 478 | } |
| 479 | |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 480 | public int getAppStandbyReason(String packageName, int userId, long elapsedRealtime) { |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 481 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 482 | AppUsageHistory appUsageHistory = |
| 483 | getPackageHistory(userHistory, packageName, elapsedRealtime, false); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 484 | return appUsageHistory != null ? appUsageHistory.bucketingReason : 0; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 485 | } |
| 486 | |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 487 | public long getElapsedTime(long elapsedRealtime) { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 488 | return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration); |
| 489 | } |
| 490 | |
Christopher Tate | a732f01 | 2017-10-26 17:26:53 -0700 | [diff] [blame] | 491 | /* Returns the new standby bucket the app is assigned to */ |
| 492 | public int setIdle(String packageName, int userId, boolean idle, long elapsedRealtime) { |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 493 | final int newBucket; |
| 494 | final int reason; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 495 | if (idle) { |
Kweku Adams | 95cd952 | 2020-05-08 09:56:53 -0700 | [diff] [blame] | 496 | newBucket = IDLE_BUCKET_CUTOFF; |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 497 | reason = REASON_MAIN_FORCED_BY_USER; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 498 | } else { |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 499 | newBucket = STANDBY_BUCKET_ACTIVE; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 500 | // This is to pretend that the app was just used, don't freeze the state anymore. |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 501 | reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 502 | } |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 503 | setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket, reason, false); |
| 504 | |
| 505 | return newBucket; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 506 | } |
| 507 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 508 | public void clearUsage(String packageName, int userId) { |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 509 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
Amith Yamasani | bdda1e0 | 2016-03-14 11:55:38 -0700 | [diff] [blame] | 510 | userHistory.remove(packageName); |
| 511 | } |
| 512 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 513 | boolean shouldInformListeners(String packageName, int userId, |
Amith Yamasani | 84cd7b7 | 2017-11-07 13:59:37 -0800 | [diff] [blame] | 514 | long elapsedRealtime, int bucket) { |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 515 | ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId); |
| 516 | AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, |
| 517 | elapsedRealtime, true); |
Amith Yamasani | 84cd7b7 | 2017-11-07 13:59:37 -0800 | [diff] [blame] | 518 | if (appUsageHistory.lastInformedBucket != bucket) { |
| 519 | appUsageHistory.lastInformedBucket = bucket; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 520 | return true; |
| 521 | } |
| 522 | return false; |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 523 | } |
| 524 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 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 | */ |
| 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 | |
Kweku Adams | 1275213 | 2020-02-18 15:36:48 -0800 | [diff] [blame] | 560 | /** |
| 561 | * Log a standby bucket change to statsd, and also logcat if debug logging is enabled. |
| 562 | */ |
| 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 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 575 | @VisibleForTesting |
| 576 | File getUserFile(int userId) { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 577 | return new File(new File(new File(mStorageDir, "users"), |
| 578 | Integer.toString(userId)), APP_IDLE_FILENAME); |
| 579 | } |
| 580 | |
Michael Wachenschwanz | d1d8aa6 | 2019-02-28 16:38:37 -0800 | [diff] [blame] | 581 | /** |
| 582 | * Check if App Idle File exists on disk |
| 583 | * @param userId |
| 584 | * @return true if file exists |
| 585 | */ |
| 586 | public boolean userFileExists(int userId) { |
| 587 | return getUserFile(userId).exists(); |
| 588 | } |
| 589 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 590 | private void readAppIdleTimes(int userId, ArrayMap<String, AppUsageHistory> userHistory) { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 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); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 616 | AppUsageHistory appUsageHistory = new AppUsageHistory(); |
| 617 | appUsageHistory.lastUsedElapsedTime = |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 618 | Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE)); |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 619 | appUsageHistory.lastUsedByUserElapsedTime = getLongValue(parser, |
| 620 | ATTR_LAST_USED_BY_USER_ELAPSED, |
| 621 | appUsageHistory.lastUsedElapsedTime); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 622 | appUsageHistory.lastUsedScreenTime = |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 623 | Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE)); |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 624 | appUsageHistory.lastPredictedTime = getLongValue(parser, |
| 625 | ATTR_LAST_PREDICTED_TIME, 0L); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 626 | String currentBucketString = parser.getAttributeValue(null, |
| 627 | ATTR_CURRENT_BUCKET); |
| 628 | appUsageHistory.currentBucket = currentBucketString == null |
Amith Yamasani | 172612c | 2017-12-15 10:51:53 -0800 | [diff] [blame] | 629 | ? STANDBY_BUCKET_ACTIVE |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 630 | : Integer.parseInt(currentBucketString); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 631 | String bucketingReason = |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 632 | parser.getAttributeValue(null, ATTR_BUCKETING_REASON); |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 633 | appUsageHistory.lastJobRunTime = getLongValue(parser, |
| 634 | ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE); |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 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); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 639 | appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; |
| 640 | if (bucketingReason != null) { |
| 641 | try { |
| 642 | appUsageHistory.bucketingReason = |
| 643 | Integer.parseInt(bucketingReason, 16); |
| 644 | } catch (NumberFormatException nfe) { |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 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); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 658 | } |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 659 | } |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 660 | appUsageHistory.lastInformedBucket = -1; |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 661 | userHistory.put(packageName, appUsageHistory); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 662 | } |
| 663 | } |
| 664 | } |
| 665 | } catch (IOException | XmlPullParserException e) { |
Michael Wachenschwanz | 16b2f2b | 2019-10-09 15:32:55 -0700 | [diff] [blame] | 666 | Slog.e(TAG, "Unable to read app idle file for user " + userId, e); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 667 | } finally { |
| 668 | IoUtils.closeQuietly(fis); |
| 669 | } |
| 670 | } |
| 671 | |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 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 | |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 678 | public void writeAppIdleTimes(int userId) { |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 679 | FileOutputStream fos = null; |
| 680 | AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); |
| 681 | try { |
| 682 | fos = appIdleFile.startWrite(); |
| 683 | final BufferedOutputStream bos = new BufferedOutputStream(fos); |
| 684 | |
| 685 | FastXmlSerializer xml = new FastXmlSerializer(); |
| 686 | xml.setOutput(bos, StandardCharsets.UTF_8.name()); |
| 687 | xml.startDocument(null, true); |
| 688 | xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); |
| 689 | |
| 690 | xml.startTag(null, TAG_PACKAGES); |
| 691 | |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 692 | ArrayMap<String,AppUsageHistory> userHistory = getUserHistory(userId); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 693 | final int N = userHistory.size(); |
| 694 | for (int i = 0; i < N; i++) { |
| 695 | String packageName = userHistory.keyAt(i); |
Michael Wachenschwanz | 16b2f2b | 2019-10-09 15:32:55 -0700 | [diff] [blame] | 696 | // Skip any unexpected null package names |
| 697 | if (packageName == null) { |
| 698 | Slog.w(TAG, "Skipping App Idle write for unexpected null package"); |
| 699 | continue; |
| 700 | } |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 701 | AppUsageHistory history = userHistory.valueAt(i); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 702 | xml.startTag(null, TAG_PACKAGE); |
| 703 | xml.attribute(null, ATTR_NAME, packageName); |
| 704 | xml.attribute(null, ATTR_ELAPSED_IDLE, |
| 705 | Long.toString(history.lastUsedElapsedTime)); |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 706 | xml.attribute(null, ATTR_LAST_USED_BY_USER_ELAPSED, |
| 707 | Long.toString(history.lastUsedByUserElapsedTime)); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 708 | xml.attribute(null, ATTR_SCREEN_IDLE, |
| 709 | Long.toString(history.lastUsedScreenTime)); |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 710 | xml.attribute(null, ATTR_LAST_PREDICTED_TIME, |
| 711 | Long.toString(history.lastPredictedTime)); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 712 | xml.attribute(null, ATTR_CURRENT_BUCKET, |
| 713 | Integer.toString(history.currentBucket)); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 714 | xml.attribute(null, ATTR_BUCKETING_REASON, |
| 715 | Integer.toHexString(history.bucketingReason)); |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 716 | if (history.bucketActiveTimeoutTime > 0) { |
| 717 | xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history |
| 718 | .bucketActiveTimeoutTime)); |
| 719 | } |
| 720 | if (history.bucketWorkingSetTimeoutTime > 0) { |
| 721 | xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history |
| 722 | .bucketWorkingSetTimeoutTime)); |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 723 | } |
| 724 | if (history.lastJobRunTime != Long.MIN_VALUE) { |
| 725 | xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history |
| 726 | .lastJobRunTime)); |
| 727 | } |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 728 | if (history.lastRestrictAttemptElapsedTime > 0) { |
| 729 | xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, |
| 730 | Long.toString(history.lastRestrictAttemptElapsedTime)); |
| 731 | } |
| 732 | xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON, |
| 733 | Integer.toHexString(history.lastRestrictReason)); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 734 | xml.endTag(null, TAG_PACKAGE); |
| 735 | } |
| 736 | |
| 737 | xml.endTag(null, TAG_PACKAGES); |
| 738 | xml.endDocument(); |
| 739 | appIdleFile.finishWrite(fos); |
| 740 | } catch (Exception e) { |
| 741 | appIdleFile.failWrite(fos); |
Michael Wachenschwanz | 16b2f2b | 2019-10-09 15:32:55 -0700 | [diff] [blame] | 742 | Slog.e(TAG, "Error writing app idle file for user " + userId, e); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 743 | } |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 744 | } |
| 745 | |
Sudheer Shanka | b497dd4 | 2020-02-16 22:23:24 -0800 | [diff] [blame] | 746 | public void dump(IndentingPrintWriter idpw, int userId, List<String> pkgs) { |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 747 | idpw.println("App Standby States:"); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 748 | idpw.increaseIndent(); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 749 | ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 750 | final long elapsedRealtime = SystemClock.elapsedRealtime(); |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 751 | final long totalElapsedTime = getElapsedTime(elapsedRealtime); |
| 752 | final long screenOnTime = getScreenOnTime(elapsedRealtime); |
Amith Yamasani | 0a11e69 | 2015-05-08 16:36:21 -0700 | [diff] [blame] | 753 | if (userHistory == null) return; |
| 754 | final int P = userHistory.size(); |
| 755 | for (int p = 0; p < P; p++) { |
| 756 | final String packageName = userHistory.keyAt(p); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 757 | final AppUsageHistory appUsageHistory = userHistory.valueAt(p); |
Sudheer Shanka | b497dd4 | 2020-02-16 22:23:24 -0800 | [diff] [blame] | 758 | if (!CollectionUtils.isEmpty(pkgs) && !pkgs.contains(packageName)) { |
Dianne Hackborn | c81983a | 2017-10-20 16:16:32 -0700 | [diff] [blame] | 759 | continue; |
| 760 | } |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 761 | idpw.print("package=" + packageName); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 762 | idpw.print(" u=" + userId); |
| 763 | idpw.print(" bucket=" + appUsageHistory.currentBucket |
| 764 | + " reason=" |
| 765 | + UsageStatsManager.reasonToString(appUsageHistory.bucketingReason)); |
| 766 | idpw.print(" used="); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 767 | TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw); |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 768 | idpw.print(" usedByUser="); |
| 769 | TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedByUserElapsedTime, |
| 770 | idpw); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 771 | idpw.print(" usedScr="); |
Amith Yamasani | 17fffee | 2017-09-29 13:17:43 -0700 | [diff] [blame] | 772 | TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 773 | idpw.print(" lastPred="); |
Amith Yamasani | bd7b302 | 2017-12-06 17:40:25 -0800 | [diff] [blame] | 774 | TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 775 | idpw.print(" activeLeft="); |
| 776 | TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime, |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 777 | idpw); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 778 | idpw.print(" wsLeft="); |
| 779 | TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime, |
Amith Yamasani | bbbad9c | 2018-02-10 16:46:38 -0800 | [diff] [blame] | 780 | idpw); |
Amith Yamasani | 119be9a | 2018-02-18 22:23:00 -0800 | [diff] [blame] | 781 | idpw.print(" lastJob="); |
Amith Yamasani | 53f06ea | 2018-01-05 17:53:46 -0800 | [diff] [blame] | 782 | TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw); |
Kweku Adams | c6a9b34 | 2020-01-08 18:37:26 -0800 | [diff] [blame] | 783 | if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) { |
| 784 | idpw.print(" lastRestrictAttempt="); |
| 785 | TimeUtils.formatDuration( |
| 786 | totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw); |
| 787 | idpw.print(" lastRestrictReason=" |
| 788 | + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason)); |
| 789 | } |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 790 | idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n")); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 791 | idpw.println(); |
| 792 | } |
| 793 | idpw.println(); |
| 794 | idpw.print("totalElapsedTime="); |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 795 | TimeUtils.formatDuration(getElapsedTime(elapsedRealtime), idpw); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 796 | idpw.println(); |
| 797 | idpw.print("totalScreenOnTime="); |
Amith Yamasani | 61d5fd7 | 2017-02-24 11:02:07 -0800 | [diff] [blame] | 798 | TimeUtils.formatDuration(getScreenOnTime(elapsedRealtime), idpw); |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 799 | idpw.println(); |
| 800 | idpw.decreaseIndent(); |
| 801 | } |
Amith Yamasani | a93542f | 2016-02-03 18:02:06 -0800 | [diff] [blame] | 802 | } |