Merge "Add a knob to change the impact of the notification-seen event."

This commit is contained in:
Sudheer Shanka
2022-01-19 22:21:01 +00:00
committed by Android (Google) Code Review
5 changed files with 376 additions and 113 deletions

View File

@@ -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<ArrayMap<String,AppUsageHistory>> 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:
* <pre>
* <package activeTimeoutTime="..." workingSetTimeoutTime="..." />
* </pre>
* In this version, it is changed to:
* <pre>
* <package>
* <expiryTimes>
* <item bucket="..." expiry="..." />
* <item bucket="..." expiry="..." />
* </expiryTimes>
* </package>
* </pre>
*/
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<String, AppUsageHistory> 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<String, AppUsageHistory> 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<String, AppUsageHistory> 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<String, AppUsageHistory> 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<String,AppUsageHistory> 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("<none>");
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(")");
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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<Integer, Long> 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<Integer, Long> 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<Integer, Long> expectedExpiryTimesMs) throws Exception {
for (Map.Entry<Integer, Long> 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);
}
}
}

View File

@@ -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 {