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