diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java index 8b175127373a1..4de8ec8d12418 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java @@ -42,6 +42,7 @@ import android.util.AtomicFile; import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseLongArray; import android.util.TimeUtils; import android.util.Xml; @@ -49,6 +50,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.CollectionUtils; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.XmlUtils; import libcore.io.IoUtils; @@ -80,7 +82,7 @@ public class AppIdleHistory { private SparseArray> mIdleHistory = new SparseArray<>(); private static final long ONE_MINUTE = 60 * 1000; - private static final int STANDBY_BUCKET_UNKNOWN = -1; + static final int STANDBY_BUCKET_UNKNOWN = -1; /** * The bucket beyond which apps are considered idle. Any apps in this bucket or lower are @@ -88,10 +90,35 @@ public class AppIdleHistory { */ static final int IDLE_BUCKET_CUTOFF = STANDBY_BUCKET_RARE; + /** Initial version of the xml containing the app idle stats ({@link #APP_IDLE_FILENAME}). */ + private static final int XML_VERSION_INITIAL = 0; + /** + * Allowed writing expiry times for any standby bucket instead of only active and working set. + * In previous version, we used to specify expiry times for active and working set as + * attributes: + *
+     *     
+     * 
+ * In this version, it is changed to: + *
+     *     
+     *         
+     *             
+     *             
+     *         
+     *     
+     * 
+ */ + private static final int XML_VERSION_ADD_BUCKET_EXPIRY_TIMES = 1; + /** Current version */ + private static final int XML_VERSION_CURRENT = XML_VERSION_ADD_BUCKET_EXPIRY_TIMES; + @VisibleForTesting static final String APP_IDLE_FILENAME = "app_idle_stats.xml"; private static final String TAG_PACKAGES = "packages"; private static final String TAG_PACKAGE = "package"; + private static final String TAG_BUCKET_EXPIRY_TIMES = "expiryTimes"; + private static final String TAG_ITEM = "item"; private static final String ATTR_NAME = "name"; // Screen on timebase time when app was last used private static final String ATTR_SCREEN_IDLE = "screenIdleTime"; @@ -111,6 +138,10 @@ public class AppIdleHistory { private static final String ATTR_BUCKET_ACTIVE_TIMEOUT_TIME = "activeTimeoutTime"; // The time when the forced working_set state can be overridden. private static final String ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME = "workingSetTimeoutTime"; + // The standby bucket value + private static final String ATTR_BUCKET = "bucket"; + // The time when the forced bucket state can be overridde. + private static final String ATTR_EXPIRY_TIME = "expiry"; // Elapsed timebase time when the app was last marked for restriction. private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED = "lastRestrictionAttemptElapsedTime"; @@ -119,6 +150,8 @@ public class AppIdleHistory { "lastRestrictionAttemptReason"; // The next estimated launch time of the app, in ms since epoch. private static final String ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME = "nextEstimatedAppLaunchTime"; + // Version of the xml file. + private static final String ATTR_VERSION = "version"; // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot) private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration @@ -158,15 +191,10 @@ public class AppIdleHistory { // The estimated time the app will be launched next, in milliseconds since epoch. @CurrentTimeMillisLong long nextEstimatedLaunchTime; - // When should the bucket active state timeout, in elapsed timebase, if greater than - // lastUsedElapsedTime. - // This is used to keep the app in a high bucket regardless of other timeouts and - // predictions. - long bucketActiveTimeoutTime; - // If there's a forced working_set state, this is when it times out. This can be sitting - // under any active state timeout, so that it becomes applicable after the active state - // timeout expires. - long bucketWorkingSetTimeoutTime; + // Contains standby buckets that apps were forced into and the corresponding expiry times + // (in elapsed timebase) for each bucket state. App will stay in the highest bucket until + // it's expiry time is elapsed and will be moved to the next highest bucket. + SparseLongArray bucketExpiryTimesMs; // The last time an agent attempted to put the app into the RESTRICTED bucket. long lastRestrictAttemptElapsedTime; // The last reason the app was marked to be put into the RESTRICTED bucket. @@ -249,21 +277,24 @@ public class AppIdleHistory { } /** - * Mark the app as used and update the bucket if necessary. If there is a timeout specified + * Mark the app as used and update the bucket if necessary. If there is a expiry time specified * that's in the future, then the usage event is temporary and keeps the app in the specified - * bucket at least until the timeout is reached. This can be used to keep the app in an + * bucket at least until the expiry time is reached. This can be used to keep the app in an * elevated bucket for a while until some important task gets to run. + * * @param appUsageHistory the usage record for the app being updated * @param packageName name of the app being updated, for logging purposes * @param newBucket the bucket to set the app to * @param usageReason the sub-reason for usage, one of REASON_SUB_USAGE_* - * @param elapsedRealtime mark as used time if non-zero - * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used - * with bucket values of ACTIVE and WORKING_SET. + * @param nowElapsedRealtimeMs mark as used time if non-zero (in + * {@link SystemClock#elapsedRealtime()} time base) + * @param expiryElapsedRealtimeMs the expiry time for the specified bucket (in + * {@link SystemClock#elapsedRealtime()} time base) * @return {@code appUsageHistory} */ AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName, int userId, - int newBucket, int usageReason, long elapsedRealtime, long timeout) { + int newBucket, int usageReason, + long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs) { int bucketingReason = REASON_MAIN_USAGE | usageReason; final boolean isUserUsage = isUserUsage(bucketingReason); @@ -274,30 +305,27 @@ public class AppIdleHistory { newBucket = STANDBY_BUCKET_RESTRICTED; bucketingReason = appUsageHistory.bucketingReason; } else { - // Set the timeout if applicable - if (timeout > elapsedRealtime) { + // Set the expiry time if applicable + if (expiryElapsedRealtimeMs > nowElapsedRealtimeMs) { // Convert to elapsed timebase - final long timeoutTime = mElapsedDuration + (timeout - mElapsedSnapshot); - if (newBucket == STANDBY_BUCKET_ACTIVE) { - appUsageHistory.bucketActiveTimeoutTime = Math.max(timeoutTime, - appUsageHistory.bucketActiveTimeoutTime); - } else if (newBucket == STANDBY_BUCKET_WORKING_SET) { - appUsageHistory.bucketWorkingSetTimeoutTime = Math.max(timeoutTime, - appUsageHistory.bucketWorkingSetTimeoutTime); - } else { - throw new IllegalArgumentException("Cannot set a timeout on bucket=" - + newBucket); + final long expiryTimeMs = getElapsedTime(expiryElapsedRealtimeMs); + if (appUsageHistory.bucketExpiryTimesMs == null) { + appUsageHistory.bucketExpiryTimesMs = new SparseLongArray(); } + final long currentExpiryTimeMs = appUsageHistory.bucketExpiryTimesMs.get(newBucket); + appUsageHistory.bucketExpiryTimesMs.put(newBucket, + Math.max(expiryTimeMs, currentExpiryTimeMs)); + removeElapsedExpiryTimes(appUsageHistory, getElapsedTime(nowElapsedRealtimeMs)); } } - if (elapsedRealtime != 0) { + if (nowElapsedRealtimeMs != 0) { appUsageHistory.lastUsedElapsedTime = mElapsedDuration - + (elapsedRealtime - mElapsedSnapshot); + + (nowElapsedRealtimeMs - mElapsedSnapshot); if (isUserUsage) { appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime; } - appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime); + appUsageHistory.lastUsedScreenTime = getScreenOnTime(nowElapsedRealtimeMs); } if (appUsageHistory.currentBucket > newBucket) { @@ -309,26 +337,41 @@ public class AppIdleHistory { return appUsageHistory; } + private void removeElapsedExpiryTimes(AppUsageHistory appUsageHistory, long elapsedTimeMs) { + if (appUsageHistory.bucketExpiryTimesMs == null) { + return; + } + for (int i = appUsageHistory.bucketExpiryTimesMs.size() - 1; i >= 0; --i) { + if (appUsageHistory.bucketExpiryTimesMs.valueAt(i) < elapsedTimeMs) { + appUsageHistory.bucketExpiryTimesMs.removeAt(i); + } + } + } + /** - * Mark the app as used and update the bucket if necessary. If there is a timeout specified + * Mark the app as used and update the bucket if necessary. If there is a expiry time specified * that's in the future, then the usage event is temporary and keeps the app in the specified - * bucket at least until the timeout is reached. This can be used to keep the app in an + * bucket at least until the expiry time is reached. This can be used to keep the app in an * elevated bucket for a while until some important task gets to run. - * @param packageName - * @param userId + * + * @param packageName package name of the app the usage is reported for + * @param userId user that the app is running in * @param newBucket the bucket to set the app to * @param usageReason sub reason for usage - * @param nowElapsed mark as used time if non-zero - * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used - * with bucket values of ACTIVE and WORKING_SET. - * @return + * @param nowElapsedRealtimeMs mark as used time if non-zero (in + * {@link SystemClock#elapsedRealtime()} time base). + * @param expiryElapsedRealtimeMs the expiry time for the specified bucket (in + * {@link SystemClock#elapsedRealtime()} time base). + * @return the {@link AppUsageHistory} corresponding to the {@code packageName} + * and {@code userId}. */ public AppUsageHistory reportUsage(String packageName, int userId, int newBucket, - int usageReason, long nowElapsed, long timeout) { + int usageReason, long nowElapsedRealtimeMs, long expiryElapsedRealtimeMs) { ArrayMap userHistory = getUserHistory(userId); - AppUsageHistory history = getPackageHistory(userHistory, packageName, nowElapsed, true); - return reportUsage(history, packageName, userId, newBucket, usageReason, nowElapsed, - timeout); + AppUsageHistory history = getPackageHistory(userHistory, packageName, + nowElapsedRealtimeMs, true); + return reportUsage(history, packageName, userId, newBucket, usageReason, + nowElapsedRealtimeMs, expiryElapsedRealtimeMs); } private ArrayMap getUserHistory(int userId) { @@ -383,7 +426,7 @@ public class AppIdleHistory { } public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime, - int bucket, int reason, boolean resetTimeout) { + int bucket, int reason, boolean resetExpiryTimes) { ArrayMap userHistory = getUserHistory(userId); AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, elapsedRealtime, true); @@ -397,9 +440,8 @@ public class AppIdleHistory { appUsageHistory.lastPredictedTime = elapsed; appUsageHistory.lastPredictedBucket = bucket; } - if (resetTimeout) { - appUsageHistory.bucketActiveTimeoutTime = elapsed; - appUsageHistory.bucketWorkingSetTimeoutTime = elapsed; + if (resetExpiryTimes && appUsageHistory.bucketExpiryTimesMs != null) { + appUsageHistory.bucketExpiryTimesMs.clear(); } if (changed) { logAppStandbyBucketChanged(packageName, userId, bucket, reason); @@ -621,6 +663,17 @@ public class AppIdleHistory { } } + @VisibleForTesting + long getBucketExpiryTimeMs(String packageName, int userId, int bucket, long elapsedRealtimeMs) { + ArrayMap userHistory = getUserHistory(userId); + AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, + elapsedRealtimeMs, true); + if (appUsageHistory.bucketExpiryTimesMs == null) { + return 0; + } + return appUsageHistory.bucketExpiryTimesMs.get(bucket, 0); + } + @VisibleForTesting File getUserFile(int userId) { return new File(new File(new File(mStorageDir, "users"), @@ -657,6 +710,7 @@ public class AppIdleHistory { if (!parser.getName().equals(TAG_PACKAGES)) { return; } + final int version = getIntValue(parser, ATTR_VERSION, XML_VERSION_INITIAL); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { if (type == XmlPullParser.START_TAG) { final String name = parser.getName(); @@ -681,10 +735,6 @@ public class AppIdleHistory { parser.getAttributeValue(null, ATTR_BUCKETING_REASON); appUsageHistory.lastJobRunTime = getLongValue(parser, ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE); - appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser, - ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L); - appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser, - ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L); appUsageHistory.bucketingReason = REASON_MAIN_DEFAULT; if (bucketingReason != null) { try { @@ -710,6 +760,26 @@ public class AppIdleHistory { ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME, 0); appUsageHistory.lastInformedBucket = -1; userHistory.put(packageName, appUsageHistory); + + if (version >= XML_VERSION_ADD_BUCKET_EXPIRY_TIMES) { + final int outerDepth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, outerDepth)) { + if (TAG_BUCKET_EXPIRY_TIMES.equals(parser.getName())) { + readBucketExpiryTimes(parser, appUsageHistory); + } + } + } else { + final long bucketActiveTimeoutTime = getLongValue(parser, + ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L); + final long bucketWorkingSetTimeoutTime = getLongValue(parser, + ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L); + if (bucketActiveTimeoutTime != 0 || bucketWorkingSetTimeoutTime != 0) { + insertBucketExpiryTime(appUsageHistory, + STANDBY_BUCKET_ACTIVE, bucketActiveTimeoutTime); + insertBucketExpiryTime(appUsageHistory, + STANDBY_BUCKET_WORKING_SET, bucketWorkingSetTimeoutTime); + } + } } } } @@ -720,21 +790,53 @@ public class AppIdleHistory { } } + private void readBucketExpiryTimes(XmlPullParser parser, AppUsageHistory appUsageHistory) + throws IOException, XmlPullParserException { + final int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (TAG_ITEM.equals(parser.getName())) { + final int bucket = getIntValue(parser, ATTR_BUCKET, STANDBY_BUCKET_UNKNOWN); + if (bucket == STANDBY_BUCKET_UNKNOWN) { + Slog.e(TAG, "Error reading the buckets expiry times"); + continue; + } + final long expiryTimeMs = getLongValue(parser, ATTR_EXPIRY_TIME, 0 /* default */); + insertBucketExpiryTime(appUsageHistory, bucket, expiryTimeMs); + } + } + } + + private void insertBucketExpiryTime(AppUsageHistory appUsageHistory, + int bucket, long expiryTimeMs) { + if (expiryTimeMs == 0) { + return; + } + if (appUsageHistory.bucketExpiryTimesMs == null) { + appUsageHistory.bucketExpiryTimesMs = new SparseLongArray(); + } + appUsageHistory.bucketExpiryTimesMs.put(bucket, expiryTimeMs); + } + private long getLongValue(XmlPullParser parser, String attrName, long defValue) { String value = parser.getAttributeValue(null, attrName); if (value == null) return defValue; return Long.parseLong(value); } + private int getIntValue(XmlPullParser parser, String attrName, int defValue) { + String value = parser.getAttributeValue(null, attrName); + if (value == null) return defValue; + return Integer.parseInt(value); + } - public void writeAppIdleTimes() { + public void writeAppIdleTimes(long elapsedRealtimeMs) { final int size = mIdleHistory.size(); for (int i = 0; i < size; i++) { - writeAppIdleTimes(mIdleHistory.keyAt(i)); + writeAppIdleTimes(mIdleHistory.keyAt(i), elapsedRealtimeMs); } } - public void writeAppIdleTimes(int userId) { + public void writeAppIdleTimes(int userId, long elapsedRealtimeMs) { FileOutputStream fos = null; AtomicFile appIdleFile = new AtomicFile(getUserFile(userId)); try { @@ -747,7 +849,9 @@ public class AppIdleHistory { xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); xml.startTag(null, TAG_PACKAGES); + xml.attribute(null, ATTR_VERSION, String.valueOf(XML_VERSION_CURRENT)); + final long elapsedTimeMs = getElapsedTime(elapsedRealtimeMs); ArrayMap userHistory = getUserHistory(userId); final int N = userHistory.size(); for (int i = 0; i < N; i++) { @@ -772,14 +876,6 @@ public class AppIdleHistory { Integer.toString(history.currentBucket)); xml.attribute(null, ATTR_BUCKETING_REASON, Integer.toHexString(history.bucketingReason)); - if (history.bucketActiveTimeoutTime > 0) { - xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history - .bucketActiveTimeoutTime)); - } - if (history.bucketWorkingSetTimeoutTime > 0) { - xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history - .bucketWorkingSetTimeoutTime)); - } if (history.lastJobRunTime != Long.MIN_VALUE) { xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history .lastJobRunTime)); @@ -794,6 +890,22 @@ public class AppIdleHistory { xml.attribute(null, ATTR_NEXT_ESTIMATED_APP_LAUNCH_TIME, Long.toString(history.nextEstimatedLaunchTime)); } + if (history.bucketExpiryTimesMs != null) { + xml.startTag(null, TAG_BUCKET_EXPIRY_TIMES); + for (int j = 0; j < history.bucketExpiryTimesMs.size(); ++j) { + final long expiryTimeMs = history.bucketExpiryTimesMs.valueAt(j); + // Skip writing to disk if the expiry time already elapsed. + if (expiryTimeMs < elapsedTimeMs) { + continue; + } + final int bucket = history.bucketExpiryTimesMs.keyAt(j); + xml.startTag(null, TAG_ITEM); + xml.attribute(null, ATTR_BUCKET, String.valueOf(bucket)); + xml.attribute(null, ATTR_EXPIRY_TIME, String.valueOf(expiryTimeMs)); + xml.endTag(null, TAG_ITEM); + } + xml.endTag(null, TAG_BUCKET_EXPIRY_TIMES); + } xml.endTag(null, TAG_PACKAGE); } @@ -846,12 +958,7 @@ public class AppIdleHistory { TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw); idpw.print(" lastPred="); TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw); - idpw.print(" activeLeft="); - TimeUtils.formatDuration(appUsageHistory.bucketActiveTimeoutTime - totalElapsedTime, - idpw); - idpw.print(" wsLeft="); - TimeUtils.formatDuration(appUsageHistory.bucketWorkingSetTimeoutTime - totalElapsedTime, - idpw); + dumpBucketExpiryTimes(idpw, appUsageHistory, totalElapsedTime); idpw.print(" lastJob="); TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw); if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) { @@ -877,4 +984,25 @@ public class AppIdleHistory { idpw.println(); idpw.decreaseIndent(); } + + private void dumpBucketExpiryTimes(IndentingPrintWriter idpw, AppUsageHistory appUsageHistory, + long totalElapsedTimeMs) { + idpw.print(" expiryTimes="); + if (appUsageHistory.bucketExpiryTimesMs == null + || appUsageHistory.bucketExpiryTimesMs.size() == 0) { + idpw.print(""); + return; + } + idpw.print("("); + for (int i = 0; i < appUsageHistory.bucketExpiryTimesMs.size(); ++i) { + final int bucket = appUsageHistory.bucketExpiryTimesMs.keyAt(i); + final long expiryTimeMs = appUsageHistory.bucketExpiryTimesMs.valueAt(i); + if (i != 0) { + idpw.print(","); + } + idpw.print(bucket + ":"); + TimeUtils.formatDuration(totalElapsedTimeMs - expiryTimeMs, idpw); + } + idpw.print(")"); + } } diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index abbae4e8e43cc..56d901738d42c 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -49,10 +49,12 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; +import static android.app.usage.UsageStatsManager.standbyBucketToString; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.server.SystemService.PHASE_BOOT_COMPLETED; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; +import static com.android.server.usage.AppIdleHistory.STANDBY_BUCKET_UNKNOWN; import android.annotation.CurrentTimeMillisLong; import android.annotation.NonNull; @@ -298,6 +300,9 @@ public class AppStandbyController long mStrongUsageTimeoutMillis = ConstantsObserver.DEFAULT_STRONG_USAGE_TIMEOUT; /** Minimum time a notification seen event should keep the bucket elevated. */ long mNotificationSeenTimeoutMillis = ConstantsObserver.DEFAULT_NOTIFICATION_TIMEOUT; + /** The standby bucket that an app will be promoted on a notification-seen event */ + int mNotificationSeenPromotedBucket = + ConstantsObserver.DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET; /** Minimum time a system update event should keep the buckets elevated. */ long mSystemUpdateUsageTimeoutMillis = ConstantsObserver.DEFAULT_SYSTEM_UPDATE_TIMEOUT; /** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */ @@ -773,7 +778,7 @@ public class AppStandbyController userId); if (DEBUG) { Slog.d(TAG, " Checking idle state for " + packageName - + " minBucket=" + minBucket); + + " minBucket=" + standbyBucketToString(minBucket)); } if (minBucket <= STANDBY_BUCKET_ACTIVE) { // No extra processing needed for ACTIVE or higher since apps can't drop into lower @@ -815,36 +820,34 @@ public class AppStandbyController newBucket = app.lastPredictedBucket; reason = REASON_MAIN_PREDICTED | REASON_SUB_PREDICTED_RESTORED; if (DEBUG) { - Slog.d(TAG, "Restored predicted newBucket = " + newBucket); + Slog.d(TAG, "Restored predicted newBucket = " + + standbyBucketToString(newBucket)); } } else { newBucket = getBucketForLocked(packageName, userId, elapsedRealtime); if (DEBUG) { - Slog.d(TAG, "Evaluated AOSP newBucket = " + newBucket); + Slog.d(TAG, "Evaluated AOSP newBucket = " + + standbyBucketToString(newBucket)); } reason = REASON_MAIN_TIMEOUT; } } - // Check if the app is within one of the timeouts for forced bucket elevation + // Check if the app is within one of the expiry times for forced bucket elevation final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime); - if (newBucket >= STANDBY_BUCKET_ACTIVE - && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) { - newBucket = STANDBY_BUCKET_ACTIVE; - reason = app.bucketingReason; - if (DEBUG) { - Slog.d(TAG, " Keeping at ACTIVE due to min timeout"); + final int bucketWithValidExpiryTime = getMinBucketWithValidExpiryTime(app, + newBucket, elapsedTimeAdjusted); + if (bucketWithValidExpiryTime != STANDBY_BUCKET_UNKNOWN) { + newBucket = bucketWithValidExpiryTime; + if (newBucket == STANDBY_BUCKET_ACTIVE || app.currentBucket == newBucket) { + reason = app.bucketingReason; + } else { + reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; } - } else if (newBucket >= STANDBY_BUCKET_WORKING_SET - && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) { - newBucket = STANDBY_BUCKET_WORKING_SET; - // If it was already there, keep the reason, else assume timeout to WS - reason = (newBucket == oldBucket) - ? app.bucketingReason - : REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; if (DEBUG) { - Slog.d(TAG, " Keeping at WORKING_SET due to min timeout"); + Slog.d(TAG, " Keeping at " + standbyBucketToString(newBucket) + + " due to min timeout"); } } @@ -868,13 +871,14 @@ public class AppStandbyController newBucket = minBucket; // Leave the reason alone. if (DEBUG) { - Slog.d(TAG, "Bringing up from " + newBucket + " to " + minBucket + Slog.d(TAG, "Bringing up from " + standbyBucketToString(newBucket) + + " to " + standbyBucketToString(minBucket) + " due to min bucketing"); } } if (DEBUG) { - Slog.d(TAG, " Old bucket=" + oldBucket - + ", newBucket=" + newBucket); + Slog.d(TAG, " Old bucket=" + standbyBucketToString(oldBucket) + + ", newBucket=" + standbyBucketToString(newBucket)); } if (oldBucket != newBucket || predictionLate) { mAppIdleHistory.setAppStandbyBucket(packageName, userId, @@ -967,6 +971,7 @@ public class AppStandbyController } } + @GuardedBy("mAppIdleLock") private void reportEventLocked(String pkg, int eventType, long elapsedRealtime, int userId) { // TODO: Ideally this should call isAppIdleFiltered() to avoid calling back // about apps that are on some kind of whitelist anyway. @@ -980,12 +985,20 @@ public class AppStandbyController final long nextCheckDelay; final int subReason = usageEventToSubReason(eventType); final int reason = REASON_MAIN_USAGE | subReason; - if (eventType == UsageEvents.Event.NOTIFICATION_SEEN - || eventType == UsageEvents.Event.SLICE_PINNED) { + if (eventType == UsageEvents.Event.NOTIFICATION_SEEN) { + // Notification-seen elevates to a higher bucket (depending on + // {@link ConstantsObserver#KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET}) but doesn't + // change usage time. + mAppIdleHistory.reportUsage(appHistory, pkg, userId, + mNotificationSeenPromotedBucket, subReason, + 0, elapsedRealtime + mNotificationSeenTimeoutMillis); + nextCheckDelay = mNotificationSeenTimeoutMillis; + } else if (eventType == UsageEvents.Event.SLICE_PINNED) { // Mild usage elevates to WORKING_SET but doesn't change usage time. mAppIdleHistory.reportUsage(appHistory, pkg, userId, STANDBY_BUCKET_WORKING_SET, subReason, 0, elapsedRealtime + mNotificationSeenTimeoutMillis); + // TODO: Add a separate setting to control the timeout for SLICE_PINNED event. nextCheckDelay = mNotificationSeenTimeoutMillis; } else if (eventType == UsageEvents.Event.SYSTEM_INTERACTION) { mAppIdleHistory.reportUsage(appHistory, pkg, userId, @@ -1021,6 +1034,29 @@ public class AppStandbyController } } + /** + * Returns the lowest standby bucket that is better than {@code targetBucket} and has an + * valid expiry time (i.e. the expiry time is not yet elapsed). + */ + private int getMinBucketWithValidExpiryTime(AppUsageHistory usageHistory, + int targetBucket, long elapsedTimeMs) { + if (usageHistory.bucketExpiryTimesMs == null) { + return STANDBY_BUCKET_UNKNOWN; + } + final int size = usageHistory.bucketExpiryTimesMs.size(); + for (int i = 0; i < size; ++i) { + final int bucket = usageHistory.bucketExpiryTimesMs.keyAt(i); + if (targetBucket <= bucket) { + break; + } + final long expiryTimeMs = usageHistory.bucketExpiryTimesMs.valueAt(i); + if (expiryTimeMs > elapsedTimeMs) { + return bucket; + } + } + return STANDBY_BUCKET_UNKNOWN; + } + /** * Note: don't call this with the lock held since it makes calls to other system services. */ @@ -1564,23 +1600,18 @@ public class AppStandbyController // ACTIVE or WORKING_SET timeout. mAppIdleHistory.updateLastPrediction(app, elapsedTimeAdjusted, newBucket); - if (newBucket > STANDBY_BUCKET_ACTIVE - && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) { - newBucket = STANDBY_BUCKET_ACTIVE; - reason = app.bucketingReason; - if (DEBUG) { - Slog.d(TAG, " Keeping at ACTIVE due to min timeout"); - } - } else if (newBucket > STANDBY_BUCKET_WORKING_SET - && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) { - newBucket = STANDBY_BUCKET_WORKING_SET; - if (app.currentBucket != newBucket) { - reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; - } else { + final int bucketWithValidExpiryTime = getMinBucketWithValidExpiryTime(app, + newBucket, elapsedTimeAdjusted); + if (bucketWithValidExpiryTime != STANDBY_BUCKET_UNKNOWN) { + newBucket = bucketWithValidExpiryTime; + if (newBucket == STANDBY_BUCKET_ACTIVE || app.currentBucket == newBucket) { reason = app.bucketingReason; + } else { + reason = REASON_MAIN_USAGE | REASON_SUB_USAGE_ACTIVE_TIMEOUT; } if (DEBUG) { - Slog.d(TAG, " Keeping at WORKING_SET due to min timeout"); + Slog.d(TAG, " Keeping at " + standbyBucketToString(newBucket) + + " due to min timeout"); } } else if (newBucket == STANDBY_BUCKET_RARE && mAllowRestrictedBucket @@ -1746,7 +1777,7 @@ public class AppStandbyController @Override public void flushToDisk() { synchronized (mAppIdleLock) { - mAppIdleHistory.writeAppIdleTimes(); + mAppIdleHistory.writeAppIdleTimes(mInjector.elapsedRealtime()); mAppIdleHistory.writeAppIdleDurations(); } } @@ -1897,7 +1928,7 @@ public class AppStandbyController } } // Immediately persist defaults to disk - mAppIdleHistory.writeAppIdleTimes(userId); + mAppIdleHistory.writeAppIdleTimes(userId, elapsedRealtime); } } @@ -1964,6 +1995,9 @@ public class AppStandbyController pw.print(" mNotificationSeenTimeoutMillis="); TimeUtils.formatDuration(mNotificationSeenTimeoutMillis, pw); pw.println(); + pw.print(" mNotificationSeenPromotedBucket="); + pw.print(standbyBucketToString(mNotificationSeenPromotedBucket)); + pw.println(); pw.print(" mSyncAdapterTimeoutMillis="); TimeUtils.formatDuration(mSyncAdapterTimeoutMillis, pw); pw.println(); @@ -2386,6 +2420,8 @@ public class AppStandbyController private static final String KEY_STRONG_USAGE_HOLD_DURATION = "strong_usage_duration"; private static final String KEY_NOTIFICATION_SEEN_HOLD_DURATION = "notification_seen_duration"; + private static final String KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET = + "notification_seen_promoted_bucket"; private static final String KEY_SYSTEM_UPDATE_HOLD_DURATION = "system_update_usage_duration"; private static final String KEY_PREDICTION_TIMEOUT = "prediction_timeout"; @@ -2428,6 +2464,8 @@ public class AppStandbyController COMPRESS_TIME ? ONE_MINUTE : 1 * ONE_HOUR; public static final long DEFAULT_NOTIFICATION_TIMEOUT = COMPRESS_TIME ? 12 * ONE_MINUTE : 12 * ONE_HOUR; + public static final int DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET = + STANDBY_BUCKET_WORKING_SET; public static final long DEFAULT_SYSTEM_UPDATE_TIMEOUT = COMPRESS_TIME ? 2 * ONE_MINUTE : 2 * ONE_HOUR; public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT = @@ -2513,6 +2551,11 @@ public class AppStandbyController KEY_NOTIFICATION_SEEN_HOLD_DURATION, DEFAULT_NOTIFICATION_TIMEOUT); break; + case KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET: + mNotificationSeenPromotedBucket = properties.getInt( + KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET, + DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET); + break; case KEY_STRONG_USAGE_HOLD_DURATION: mStrongUsageTimeoutMillis = properties.getLong( KEY_STRONG_USAGE_HOLD_DURATION, DEFAULT_STRONG_USAGE_TIMEOUT); diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 630b8d26f52e9..d7e197ee5ed16 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -1277,6 +1277,28 @@ public final class UsageStatsManager { } } + /** @hide */ + public static String standbyBucketToString(int standbyBucket) { + switch (standbyBucket) { + case STANDBY_BUCKET_EXEMPTED: + return "EXEMPTED"; + case STANDBY_BUCKET_ACTIVE: + return "ACTIVE"; + case STANDBY_BUCKET_WORKING_SET: + return "WORKING_SET"; + case STANDBY_BUCKET_FREQUENT: + return "FREQUENT"; + case STANDBY_BUCKET_RARE: + return "RARE"; + case STANDBY_BUCKET_RESTRICTED: + return "RESTRICTED"; + case STANDBY_BUCKET_NEVER: + return "NEVER"; + default: + return String.valueOf(standbyBucket); + } + } + /** * {@hide} * Temporarily allowlist the specified app for a short duration. This is to allow an app diff --git a/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java index 88f368e403a66..06726b031a369 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java @@ -22,16 +22,20 @@ import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT; import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE; import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BACKGROUND_RESOURCE_USAGE; import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_FOREGROUND; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_NOTIFICATION_SEEN; +import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SLICE_PINNED; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; +import static android.app.usage.UsageStatsManager.standbyBucketToString; import android.os.FileUtils; import android.test.AndroidTestCase; import java.io.File; +import java.util.Map; public class AppIdleHistoryTests extends AndroidTestCase { @@ -65,7 +69,7 @@ public class AppIdleHistoryTests extends AndroidTestCase { // Screen On time file should be written right away assertTrue(aih.getScreenOnTimeFile().exists()); - aih.writeAppIdleTimes(USER_ID); + aih.writeAppIdleTimes(USER_ID, /* elapsedRealtime= */ 2000); // stats file should be written now assertTrue(new File(new File(mStorageDir, "users/" + USER_ID), AppIdleHistory.APP_IDLE_FILENAME).exists()); @@ -128,7 +132,7 @@ public class AppIdleHistoryTests extends AndroidTestCase { // Check persistence aih.writeAppIdleDurations(); - aih.writeAppIdleTimes(USER_ID); + aih.writeAppIdleTimes(USER_ID, /* elapsedRealtime= */ 3000); aih = new AppIdleHistory(mStorageDir, 4000); assertEquals(aih.getAppStandbyBucket(PACKAGE_1, USER_ID, 5000), STANDBY_BUCKET_RARE); assertEquals(aih.getAppStandbyBucket(PACKAGE_2, USER_ID, 5000), STANDBY_BUCKET_ACTIVE); @@ -165,7 +169,7 @@ public class AppIdleHistoryTests extends AndroidTestCase { aih.getAppStandbyReason(PACKAGE_1, USER_ID, 3000)); aih.setAppStandbyBucket(PACKAGE_1, USER_ID, 4000, STANDBY_BUCKET_WORKING_SET, REASON_MAIN_TIMEOUT); - aih.writeAppIdleTimes(USER_ID); + aih.writeAppIdleTimes(USER_ID, /* elapsedRealtime= */ 4000); aih = new AppIdleHistory(mStorageDir, 5000); assertEquals(REASON_MAIN_TIMEOUT, aih.getAppStandbyReason(PACKAGE_1, USER_ID, 5000)); @@ -180,11 +184,63 @@ public class AppIdleHistoryTests extends AndroidTestCase { aih.reportUsage(null, USER_ID, STANDBY_BUCKET_ACTIVE, REASON_SUB_USAGE_MOVE_TO_FOREGROUND, 2000, 0); // Persist data - aih.writeAppIdleTimes(USER_ID); + aih.writeAppIdleTimes(USER_ID, /* elapsedRealtime= */ 2000); // Recover data from disk aih = new AppIdleHistory(mStorageDir, 5000); // Verify data is intact assertEquals(REASON_MAIN_USAGE | REASON_SUB_USAGE_MOVE_TO_FOREGROUND, aih.getAppStandbyReason(PACKAGE_1, USER_ID, 3000)); } + + public void testBucketExpiryTimes() throws Exception { + AppIdleHistory aih = new AppIdleHistory(mStorageDir, 1000 /* elapsedRealtime */); + aih.reportUsage(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET, + REASON_SUB_USAGE_SLICE_PINNED, + 2000 /* elapsedRealtime */, 6000 /* expiryRealtime */); + assertEquals(5000 /* expectedExpiryTimeMs */, aih.getBucketExpiryTimeMs(PACKAGE_1, USER_ID, + STANDBY_BUCKET_WORKING_SET, 2000 /* elapsedRealtime */)); + aih.reportUsage(PACKAGE_2, USER_ID, STANDBY_BUCKET_FREQUENT, + REASON_SUB_USAGE_NOTIFICATION_SEEN, + 2000 /* elapsedRealtime */, 3000 /* expiryRealtime */); + assertEquals(2000 /* expectedExpiryTimeMs */, aih.getBucketExpiryTimeMs(PACKAGE_2, USER_ID, + STANDBY_BUCKET_FREQUENT, 2000 /* elapsedRealtime */)); + aih.writeAppIdleTimes(USER_ID, 4000 /* elapsedRealtime */); + + // Persist data + aih = new AppIdleHistory(mStorageDir, 5000 /* elapsedRealtime */); + final Map expectedExpiryTimes1 = Map.of( + STANDBY_BUCKET_ACTIVE, 0L, + STANDBY_BUCKET_WORKING_SET, 5000L, + STANDBY_BUCKET_FREQUENT, 0L, + STANDBY_BUCKET_RARE, 0L, + STANDBY_BUCKET_RESTRICTED, 0L + ); + // For PACKAGE_1, only WORKING_SET bucket should have an expiry time. + verifyBucketExpiryTimes(aih, PACKAGE_1, USER_ID, 5000 /* elapsedRealtime */, + expectedExpiryTimes1); + final Map expectedExpiryTimes2 = Map.of( + STANDBY_BUCKET_ACTIVE, 0L, + STANDBY_BUCKET_WORKING_SET, 0L, + STANDBY_BUCKET_FREQUENT, 0L, + STANDBY_BUCKET_RARE, 0L, + STANDBY_BUCKET_RESTRICTED, 0L + ); + // For PACKAGE_2, there shouldn't be any expiry time since the one set earlier would have + // elapsed by the time the data was persisted to disk + verifyBucketExpiryTimes(aih, PACKAGE_2, USER_ID, 5000 /* elapsedRealtime */, + expectedExpiryTimes2); + } + + private void verifyBucketExpiryTimes(AppIdleHistory aih, String packageName, int userId, + long elapsedRealtimeMs, Map expectedExpiryTimesMs) throws Exception { + for (Map.Entry entry : expectedExpiryTimesMs.entrySet()) { + final int bucket = entry.getKey(); + final long expectedExpiryTimeMs = entry.getValue(); + final long actualExpiryTimeMs = aih.getBucketExpiryTimeMs(packageName, userId, bucket, + elapsedRealtimeMs); + assertEquals("Unexpected expiry time for pkg=" + packageName + ", userId=" + userId + + ", bucket=" + standbyBucketToString(bucket), + expectedExpiryTimeMs, actualExpiryTimeMs); + } + } } \ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index 949ee01d68721..18d3f3d0e8056 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -63,7 +63,6 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -828,7 +827,6 @@ public class AppStandbyControllerTests { } @Test - @FlakyTest(bugId = 185169504) public void testNotificationEvent() throws Exception { reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1); assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1)); @@ -841,6 +839,22 @@ public class AppStandbyControllerTests { assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); } + @Test + public void testNotificationEvent_changePromotedBucket() throws Exception { + mController.forceIdleState(PACKAGE_1, USER_ID, true); + reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); + assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1)); + + // TODO: Avoid hardcoding these string constants. + mInjector.mSettingsBuilder.setInt("notification_seen_promoted_bucket", + STANDBY_BUCKET_FREQUENT); + mInjector.mPropertiesChangedListener.onPropertiesChanged( + mInjector.getDeviceConfigProperties()); + mController.forceIdleState(PACKAGE_1, USER_ID, true); + reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1); + assertEquals(STANDBY_BUCKET_FREQUENT, getStandbyBucket(mController, PACKAGE_1)); + } + @Test @FlakyTest(bugId = 185169504) public void testSlicePinnedEvent() throws Exception {