summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java13
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java8
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java111
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java123
-rw-r--r--api/current.txt1
-rw-r--r--core/java/android/app/usage/UsageStatsManager.java54
-rw-r--r--core/res/res/values/strings.xml4
-rw-r--r--core/res/res/values/symbols.xml3
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java34
-rw-r--r--services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java142
11 files changed, 453 insertions, 42 deletions
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index 6109b713de24..d2d942a4a7e5 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -103,6 +103,8 @@ public interface AppStandbyInternal {
/**
* Changes an app's standby bucket to the provided value. The caller can only set the standby
* bucket for a different app than itself.
+ * If attempting to automatically place an app in the RESTRICTED bucket, use
+ * {@link #restrictApp(String, int, int)} instead.
*/
void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId, int callingUid,
int callingPid);
@@ -113,6 +115,17 @@ public interface AppStandbyInternal {
void setAppStandbyBuckets(@NonNull List<AppStandbyInfo> appBuckets, int userId, int callingUid,
int callingPid);
+ /**
+ * Put the specified app in the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+ * bucket. If it has been used by the user recently, the restriction will delayed until an
+ * appropriate time.
+ *
+ * @param restrictReason The restrictReason for restricting the app. Should be one of the
+ * UsageStatsManager.REASON_SUB_RESTRICT_* reasons.
+ */
+ void restrictApp(@NonNull String packageName, int userId, int restrictReason);
+
void addActiveDeviceAdmin(String adminPkg, int userId);
void setActiveAdminApps(Set<String> adminPkgs, int userId);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 102e8485aac5..c9d092a6078e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -277,6 +277,7 @@ public class JobSchedulerService extends com.android.server.SystemService
DeviceIdleInternal mLocalDeviceIdleController;
AppStateTracker mAppStateTracker;
final UsageStatsManagerInternal mUsageStats;
+ private final AppStandbyInternal mAppStandbyInternal;
/**
* Set to true once we are allowed to run third party apps.
@@ -1062,7 +1063,8 @@ public class JobSchedulerService extends com.android.server.SystemService
packageName == null ? job.getService().getPackageName() : packageName;
if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) {
Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times");
- // TODO(b/145551233): attempt to restrict app
+ mAppStandbyInternal.restrictApp(
+ pkg, userId, UsageStatsManager.REASON_SUB_RESTRICT_BUGGY);
if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION
&& mPlatformCompat.isChangeEnabledByPackageName(
CRASH_ON_EXCEEDED_LIMIT, pkg, userId)) {
@@ -1430,8 +1432,8 @@ public class JobSchedulerService extends com.android.server.SystemService
mConstants.API_QUOTA_SCHEDULE_COUNT,
mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
- AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class);
- appStandby.addListener(mStandbyTracker);
+ mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
+ mAppStandbyInternal.addListener(mStandbyTracker);
// The job store needs to call back
publishLocalService(JobSchedulerInternal.class, new LocalService());
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 b9df30aa4d95..9d6e012da89b 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -25,8 +25,11 @@ import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACT
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
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 com.android.server.usage.AppStandbyController.isUserUsage;
+
import android.app.usage.AppStandbyInfo;
import android.app.usage.UsageStatsManager;
import android.os.SystemClock;
@@ -81,6 +84,8 @@ public class AppIdleHistory {
private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
// Elapsed timebase time when app was last used
private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
+ // Elapsed timebase time when app was last used by the user
+ private static final String ATTR_LAST_USED_BY_USER_ELAPSED = "lastUsedByUserElapsedTime";
// Elapsed timebase time when the app bucket was last predicted externally
private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime";
// The standby bucket for the app
@@ -93,6 +98,12 @@ 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";
+ // Elapsed timebase time when the app was last marked for restriction.
+ private static final String ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED =
+ "lastRestrictionAttemptElapsedTime";
+ // Reason why the app was last marked for restriction.
+ private static final String ATTR_LAST_RESTRICTION_ATTEMPT_REASON =
+ "lastRestrictionAttemptReason";
// device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
@@ -107,8 +118,10 @@ public class AppIdleHistory {
private boolean mScreenOn;
static class AppUsageHistory {
- // Last used time using elapsed timebase
+ // Last used time (including system usage), using elapsed timebase
long lastUsedElapsedTime;
+ // Last time the user used the app, using elapsed timebase
+ long lastUsedByUserElapsedTime;
// Last used time using screen_on timebase
long lastUsedScreenTime;
// Last predicted time using elapsed timebase
@@ -136,6 +149,10 @@ public class AppIdleHistory {
// under any active state timeout, so that it becomes applicable after the active state
// timeout expires.
long bucketWorkingSetTimeoutTime;
+ // 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.
+ int lastRestrictReason;
}
AppIdleHistory(File storageDir, long elapsedRealtime) {
@@ -229,25 +246,37 @@ public class AppIdleHistory {
*/
public AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName,
int newBucket, int usageReason, long elapsedRealtime, long timeout) {
- // Set the timeout if applicable
- if (timeout > elapsedRealtime) {
- // 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);
+ int bucketingReason = REASON_MAIN_USAGE | usageReason;
+ final boolean isUserUsage = isUserUsage(bucketingReason);
+
+ if (appUsageHistory.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage) {
+ // Only user usage should bring an app out of the RESTRICTED bucket.
+ newBucket = STANDBY_BUCKET_RESTRICTED;
+ bucketingReason = appUsageHistory.bucketingReason;
+ } else {
+ // Set the timeout if applicable
+ if (timeout > elapsedRealtime) {
+ // 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);
+ }
}
}
if (elapsedRealtime != 0) {
appUsageHistory.lastUsedElapsedTime = mElapsedDuration
+ (elapsedRealtime - mElapsedSnapshot);
+ if (isUserUsage) {
+ appUsageHistory.lastUsedByUserElapsedTime = appUsageHistory.lastUsedElapsedTime;
+ }
appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
}
@@ -259,7 +288,7 @@ public class AppIdleHistory {
+ ", reason=0x0" + Integer.toHexString(appUsageHistory.bucketingReason));
}
}
- appUsageHistory.bucketingReason = REASON_MAIN_USAGE | usageReason;
+ appUsageHistory.bucketingReason = bucketingReason;
return appUsageHistory;
}
@@ -386,6 +415,24 @@ public class AppIdleHistory {
}
/**
+ * Notes an attempt to put the app in the {@link UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+ * bucket.
+ *
+ * @param packageName The package name of the app that is being restricted
+ * @param userId The ID of the user in which the app is being restricted
+ * @param elapsedRealtime The time the attempt was made, in the (unadjusted) elapsed realtime
+ * timebase
+ * @param reason The reason for the restriction attempt
+ */
+ void noteRestrictionAttempt(String packageName, int userId, long elapsedRealtime, int reason) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ appUsageHistory.lastRestrictAttemptElapsedTime = getElapsedTime(elapsedRealtime);
+ appUsageHistory.lastRestrictReason = reason;
+ }
+
+ /**
* Returns the time since the last job was run for this app. This can be larger than the
* current elapsedRealtime, in case it happened before boot or a really large value if no jobs
* were ever run.
@@ -547,6 +594,9 @@ public class AppIdleHistory {
AppUsageHistory appUsageHistory = new AppUsageHistory();
appUsageHistory.lastUsedElapsedTime =
Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
+ appUsageHistory.lastUsedByUserElapsedTime = getLongValue(parser,
+ ATTR_LAST_USED_BY_USER_ELAPSED,
+ appUsageHistory.lastUsedElapsedTime);
appUsageHistory.lastUsedScreenTime =
Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
appUsageHistory.lastPredictedTime = getLongValue(parser,
@@ -570,6 +620,19 @@ public class AppIdleHistory {
appUsageHistory.bucketingReason =
Integer.parseInt(bucketingReason, 16);
} catch (NumberFormatException nfe) {
+ Slog.wtf(TAG, "Unable to read bucketing reason", nfe);
+ }
+ }
+ appUsageHistory.lastRestrictAttemptElapsedTime =
+ getLongValue(parser, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED, 0);
+ String lastRestrictReason = parser.getAttributeValue(
+ null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON);
+ if (lastRestrictReason != null) {
+ try {
+ appUsageHistory.lastRestrictReason =
+ Integer.parseInt(lastRestrictReason, 16);
+ } catch (NumberFormatException nfe) {
+ Slog.wtf(TAG, "Unable to read last restrict reason", nfe);
}
}
appUsageHistory.lastInformedBucket = -1;
@@ -618,6 +681,8 @@ public class AppIdleHistory {
xml.attribute(null, ATTR_NAME, packageName);
xml.attribute(null, ATTR_ELAPSED_IDLE,
Long.toString(history.lastUsedElapsedTime));
+ xml.attribute(null, ATTR_LAST_USED_BY_USER_ELAPSED,
+ Long.toString(history.lastUsedByUserElapsedTime));
xml.attribute(null, ATTR_SCREEN_IDLE,
Long.toString(history.lastUsedScreenTime));
xml.attribute(null, ATTR_LAST_PREDICTED_TIME,
@@ -638,6 +703,12 @@ public class AppIdleHistory {
xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history
.lastJobRunTime));
}
+ if (history.lastRestrictAttemptElapsedTime > 0) {
+ xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_ELAPSED,
+ Long.toString(history.lastRestrictAttemptElapsedTime));
+ }
+ xml.attribute(null, ATTR_LAST_RESTRICTION_ATTEMPT_REASON,
+ Integer.toHexString(history.lastRestrictReason));
xml.endTag(null, TAG_PACKAGE);
}
@@ -672,6 +743,9 @@ public class AppIdleHistory {
+ UsageStatsManager.reasonToString(appUsageHistory.bucketingReason));
idpw.print(" used=");
TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw);
+ idpw.print(" usedByUser=");
+ TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedByUserElapsedTime,
+ idpw);
idpw.print(" usedScr=");
TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
idpw.print(" lastPred=");
@@ -684,6 +758,13 @@ public class AppIdleHistory {
idpw);
idpw.print(" lastJob=");
TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
+ if (appUsageHistory.lastRestrictAttemptElapsedTime > 0) {
+ idpw.print(" lastRestrictAttempt=");
+ TimeUtils.formatDuration(
+ totalElapsedTime - appUsageHistory.lastRestrictAttemptElapsedTime, idpw);
+ idpw.print(" lastRestrictReason="
+ + UsageStatsManager.reasonToString(appUsageHistory.lastRestrictReason));
+ }
idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
idpw.println();
}
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 eb0b54b1d9fc..b1b8fba78ab9 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -23,6 +23,7 @@ import static android.app.usage.UsageStatsManager.REASON_MAIN_MASK;
import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED;
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_MASK;
import static android.app.usage.UsageStatsManager.REASON_SUB_PREDICTED_RESTORED;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_ACTIVE_TIMEOUT;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_DOZE;
@@ -44,6 +45,7 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
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 com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
@@ -73,6 +75,7 @@ import android.net.Network;
import android.net.NetworkRequest;
import android.net.NetworkScoreManager;
import android.os.BatteryStats;
+import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IDeviceIdleController;
@@ -93,7 +96,9 @@ import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.view.Display;
+import android.widget.Toast;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
@@ -124,7 +129,7 @@ import java.util.concurrent.CountDownLatch;
public class AppStandbyController implements AppStandbyInternal {
private static final String TAG = "AppStandbyController";
- static final boolean DEBUG = false;
+ static final boolean DEBUG = true;
static final boolean COMPRESS_TIME = false;
private static final long ONE_MINUTE = 60 * 1000;
@@ -615,6 +620,16 @@ public class AppStandbyController implements AppStandbyInternal {
Slog.d(TAG, " Keeping at WORKING_SET due to min timeout");
}
}
+
+ if (app.lastRestrictAttemptElapsedTime > app.lastUsedByUserElapsedTime
+ && elapsedTimeAdjusted - app.lastUsedByUserElapsedTime
+ >= mInjector.getRestrictedBucketDelayMs()) {
+ newBucket = STANDBY_BUCKET_RESTRICTED;
+ reason = app.lastRestrictReason;
+ if (DEBUG) {
+ Slog.d(TAG, "Bringing down to RESTRICTED due to timeout");
+ }
+ }
if (DEBUG) {
Slog.d(TAG, " Old bucket=" + oldBucket
+ ", newBucket=" + newBucket);
@@ -733,15 +748,16 @@ public class AppStandbyController implements AppStandbyInternal {
elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
nextCheckTime = mStrongUsageTimeoutMillis;
}
- mHandler.sendMessageDelayed(mHandler.obtainMessage
- (MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg),
- nextCheckTime);
- final boolean userStartedInteracting =
- appHistory.currentBucket == STANDBY_BUCKET_ACTIVE &&
- prevBucket != appHistory.currentBucket &&
- (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
- maybeInformListeners(pkg, userId, elapsedRealtime,
- appHistory.currentBucket, reason, userStartedInteracting);
+ if (appHistory.currentBucket != prevBucket) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkg),
+ nextCheckTime);
+ final boolean userStartedInteracting =
+ appHistory.currentBucket == STANDBY_BUCKET_ACTIVE
+ && (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
+ maybeInformListeners(pkg, userId, elapsedRealtime,
+ appHistory.currentBucket, reason, userStartedInteracting);
+ }
if (previouslyIdle) {
notifyBatteryStats(pkg, userId, false);
@@ -923,6 +939,15 @@ public class AppStandbyController implements AppStandbyInternal {
}
}
+ static boolean isUserUsage(int reason) {
+ if ((reason & REASON_MAIN_MASK) == REASON_MAIN_USAGE) {
+ final int subReason = reason & REASON_SUB_MASK;
+ return subReason == REASON_SUB_USAGE_USER_INTERACTION
+ || subReason == REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
+ }
+ return false;
+ }
+
@Override
public int[] getIdleUidsForUser(int userId) {
if (!mAppIdleEnabled) {
@@ -1017,6 +1042,20 @@ public class AppStandbyController implements AppStandbyInternal {
}
@Override
+ public void restrictApp(@NonNull String packageName, int userId, int restrictReason) {
+ // If the package is not installed, don't allow the bucket to be set.
+ if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
+ Slog.e(TAG, "Tried to restrict uninstalled app: " + packageName);
+ return;
+ }
+
+ final int reason = REASON_MAIN_FORCED_BY_SYSTEM | (REASON_SUB_MASK & restrictReason);
+ final long nowElapsed = mInjector.elapsedRealtime();
+ setAppStandbyBucket(packageName, userId, STANDBY_BUCKET_RESTRICTED, reason,
+ nowElapsed, false);
+ }
+
+ @Override
public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId,
int callingUid, int callingPid) {
setAppStandbyBuckets(
@@ -1080,6 +1119,7 @@ public class AppStandbyController implements AppStandbyInternal {
synchronized (mAppIdleLock) {
// If the package is not installed, don't allow the bucket to be set.
if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
+ Slog.e(TAG, "Tried to set bucket of uninstalled app: " + packageName);
return;
}
AppIdleHistory.AppUsageHistory app = mAppIdleHistory.getAppUsageHistory(packageName,
@@ -1089,8 +1129,9 @@ public class AppStandbyController implements AppStandbyInternal {
// Don't allow changing bucket if higher than ACTIVE
if (app.currentBucket < STANDBY_BUCKET_ACTIVE) return;
- // Don't allow prediction to change from/to NEVER
+ // Don't allow prediction to change from/to NEVER or from RESTRICTED.
if ((app.currentBucket == STANDBY_BUCKET_NEVER
+ || app.currentBucket == STANDBY_BUCKET_RESTRICTED
|| newBucket == STANDBY_BUCKET_NEVER)
&& predicted) {
return;
@@ -1103,6 +1144,50 @@ public class AppStandbyController implements AppStandbyInternal {
return;
}
+ final boolean isForcedByUser =
+ (reason & REASON_MAIN_MASK) == REASON_MAIN_FORCED_BY_USER;
+
+ // If the current bucket is RESTRICTED, only user force or usage should bring it out.
+ if (app.currentBucket == STANDBY_BUCKET_RESTRICTED && !isUserUsage(reason)
+ && !isForcedByUser) {
+ return;
+ }
+
+ if (newBucket == STANDBY_BUCKET_RESTRICTED) {
+ mAppIdleHistory
+ .noteRestrictionAttempt(packageName, userId, elapsedRealtime, reason);
+
+ if (isForcedByUser) {
+ // Only user force can bypass the delay restriction. If the user forced the
+ // app into the RESTRICTED bucket, then a toast confirming the action
+ // shouldn't be surprising.
+ if (Build.IS_DEBUGGABLE) {
+ Toast.makeText(mContext,
+ // Since AppStandbyController sits low in the lock hierarchy,
+ // make sure not to call out with the lock held.
+ mHandler.getLooper(),
+ mContext.getResources().getString(
+ R.string.as_app_forced_to_restricted_bucket, packageName),
+ Toast.LENGTH_SHORT)
+ .show();
+ } else {
+ Slog.i(TAG, packageName + " restricted by user");
+ }
+ } else {
+ final long timeUntilRestrictPossibleMs = app.lastUsedByUserElapsedTime
+ + mInjector.getRestrictedBucketDelayMs() - elapsedRealtime;
+ if (timeUntilRestrictPossibleMs > 0) {
+ Slog.w(TAG, "Tried to restrict recently used app: " + packageName
+ + " due to " + reason);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, packageName),
+ timeUntilRestrictPossibleMs);
+ return;
+ }
+ }
+ }
+
// If the bucket is required to stay in a higher state for a specified duration, don't
// override unless the duration has passed
if (predicted) {
@@ -1435,6 +1520,12 @@ public class AppStandbyController implements AppStandbyInternal {
private DisplayManager mDisplayManager;
private PowerManager mPowerManager;
int mBootPhase;
+ /**
+ * The minimum amount of time required since the last user interaction before an app can be
+ * placed in the RESTRICTED bucket.
+ */
+ // TODO: make configurable via DeviceConfig
+ private long mRestrictedBucketDelayMs = ONE_DAY;
Injector(Context context, Looper looper) {
mContext = context;
@@ -1459,6 +1550,12 @@ public class AppStandbyController implements AppStandbyInternal {
mDisplayManager = (DisplayManager) mContext.getSystemService(
Context.DISPLAY_SERVICE);
mPowerManager = mContext.getSystemService(PowerManager.class);
+
+ final ActivityManager activityManager =
+ (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ if (activityManager.isLowRamDevice() || ActivityManager.isSmallBatteryDevice()) {
+ mRestrictedBucketDelayMs = 12 * ONE_HOUR;
+ }
}
mBootPhase = phase;
}
@@ -1498,6 +1595,10 @@ public class AppStandbyController implements AppStandbyInternal {
return Environment.getDataSystemDirectory();
}
+ long getRestrictedBucketDelayMs() {
+ return mRestrictedBucketDelayMs;
+ }
+
void noteEvent(int event, String packageName, int uid) throws RemoteException {
mBatteryStats.noteEvent(event, packageName, uid);
}
diff --git a/api/current.txt b/api/current.txt
index 5575675a92c5..7422aface3e8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8030,6 +8030,7 @@ package android.app.usage {
field public static final int STANDBY_BUCKET_ACTIVE = 10; // 0xa
field public static final int STANDBY_BUCKET_FREQUENT = 30; // 0x1e
field public static final int STANDBY_BUCKET_RARE = 40; // 0x28
+ field public static final int STANDBY_BUCKET_RESTRICTED = 45; // 0x2d
field public static final int STANDBY_BUCKET_WORKING_SET = 20; // 0x14
}
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index a60e591dd0e6..5668944dfd4e 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -142,7 +142,7 @@ public final class UsageStatsManager {
/**
* The app has not be used for several days and/or is unlikely to be used for several days.
- * Apps in this bucket will have the most restrictions, including network restrictions, except
+ * Apps in this bucket will have more restrictions, including network restrictions, except
* during certain short periods (at a minimum, once a day) when they are allowed to execute
* jobs, access the network, etc.
* @see #getAppStandbyBucket()
@@ -150,6 +150,15 @@ public final class UsageStatsManager {
public static final int STANDBY_BUCKET_RARE = 40;
/**
+ * The app has not be used for several days, is unlikely to be used for several days, and has
+ * been misbehaving in some manner.
+ * Apps in this bucket will have the most restrictions, including network restrictions and
+ * additional restrictions on jobs.
+ * @see #getAppStandbyBucket()
+ */
+ public static final int STANDBY_BUCKET_RESTRICTED = 45;
+
+ /**
* The app has never been used.
* {@hide}
*/
@@ -278,6 +287,26 @@ public final class UsageStatsManager {
* @hide
*/
public static final int REASON_SUB_PREDICTED_RESTORED = 0x0001;
+ /**
+ * The reason for restricting the app is unknown or undefined.
+ * @hide
+ */
+ public static final int REASON_SUB_RESTRICT_UNDEFINED = 0x0000;
+ /**
+ * The app was unnecessarily using system resources (battery, memory, etc) in the background.
+ * @hide
+ */
+ public static final int REASON_SUB_RESTRICT_BACKGROUND_RESOURCE_USAGE = 0x0001;
+ /**
+ * The app was deemed to be intentionally abusive.
+ * @hide
+ */
+ public static final int REASON_SUB_RESTRICT_ABUSE = 0x0002;
+ /**
+ * The app was displaying buggy behavior.
+ * @hide
+ */
+ public static final int REASON_SUB_RESTRICT_BUGGY = 0x0003;
/** @hide */
@@ -287,6 +316,7 @@ public final class UsageStatsManager {
STANDBY_BUCKET_WORKING_SET,
STANDBY_BUCKET_FREQUENT,
STANDBY_BUCKET_RARE,
+ STANDBY_BUCKET_RESTRICTED,
STANDBY_BUCKET_NEVER,
})
@Retention(RetentionPolicy.SOURCE)
@@ -598,7 +628,7 @@ public final class UsageStatsManager {
* state of the app based on app usage patterns. Standby buckets determine how much an app will
* be restricted from running background tasks such as jobs and alarms.
* <p>Restrictions increase progressively from {@link #STANDBY_BUCKET_ACTIVE} to
- * {@link #STANDBY_BUCKET_RARE}, with {@link #STANDBY_BUCKET_ACTIVE} being the least
+ * {@link #STANDBY_BUCKET_RESTRICTED}, with {@link #STANDBY_BUCKET_ACTIVE} being the least
* restrictive. The battery level of the device might also affect the restrictions.
* <p>Apps in buckets &le; {@link #STANDBY_BUCKET_ACTIVE} have no standby restrictions imposed.
* Apps in buckets &gt; {@link #STANDBY_BUCKET_FREQUENT} may have network access restricted when
@@ -642,7 +672,8 @@ public final class UsageStatsManager {
/**
* {@hide}
* Changes an app's standby bucket to the provided value. The caller can only set the standby
- * bucket for a different app than itself.
+ * bucket for a different app than itself. The caller will not be able to change an app's
+ * standby bucket if that app is in the {@link #STANDBY_BUCKET_RESTRICTED} bucket.
* @param packageName the package name of the app to set the bucket for. A SecurityException
* will be thrown if the package name is that of the caller.
* @param bucket the standby bucket to set it to, which should be one of STANDBY_BUCKET_*.
@@ -688,7 +719,8 @@ public final class UsageStatsManager {
/**
* {@hide}
* Changes the app standby bucket for multiple apps at once. The Map is keyed by the package
- * name and the value is one of STANDBY_BUCKET_*.
+ * name and the value is one of STANDBY_BUCKET_*. The caller will not be able to change an
+ * app's standby bucket if that app is in the {@link #STANDBY_BUCKET_RESTRICTED} bucket.
* @param appBuckets a map of package name to bucket value.
*/
@SystemApi
@@ -1027,6 +1059,20 @@ public final class UsageStatsManager {
break;
case REASON_MAIN_FORCED_BY_SYSTEM:
sb.append("s");
+ switch (standbyReason & REASON_SUB_MASK) {
+ case REASON_SUB_RESTRICT_ABUSE:
+ sb.append("-ra");
+ break;
+ case REASON_SUB_RESTRICT_BACKGROUND_RESOURCE_USAGE:
+ sb.append("-rbru");
+ break;
+ case REASON_SUB_RESTRICT_BUGGY:
+ sb.append("-rb");
+ break;
+ case REASON_SUB_RESTRICT_UNDEFINED:
+ sb.append("-r");
+ break;
+ }
break;
case REASON_MAIN_FORCED_BY_USER:
sb.append("f");
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6a00ecbe91bc..bed418dc4c2d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5315,4 +5315,8 @@
<!-- Accessibility description of caption view -->
<string name="accessibility_freeform_caption">Caption bar of <xliff:g id="app_name">%1$s</xliff:g>.</string>
+
+ <!-- Text to tell the user that a package has been forced by themselves in the RESTRICTED bucket. [CHAR LIMIT=NONE] -->
+ <string name="as_app_forced_to_restricted_bucket">
+ <xliff:g id="package_name" example="com.android.example">%1$s</xliff:g> has been put into the RESTRICTED bucket</string>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 7e6eb5de25a2..357b1f069107 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3810,6 +3810,9 @@
<java-symbol type="string" name="config_rawContactsLocalAccountName" />
<java-symbol type="string" name="config_rawContactsLocalAccountType" />
+ <!-- For App Standby -->
+ <java-symbol type="string" name="as_app_forced_to_restricted_bucket" />
+
<!-- Assistant handles -->
<java-symbol type="dimen" name="assist_handle_shadow_radius" />
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 3f0e2ce9ed13..53a967b0ce50 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -2391,6 +2391,8 @@ final class ActivityManagerShellCommand extends ShellCommand {
return UsageStatsManager.STANDBY_BUCKET_FREQUENT;
} else if (lower.startsWith("ra")) {
return UsageStatsManager.STANDBY_BUCKET_RARE;
+ } else if (lower.startsWith("re")) {
+ return UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
} else if (lower.startsWith("ne")) {
return UsageStatsManager.STANDBY_BUCKET_NEVER;
} else {
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 4a13dce5642b..7af3ec62e651 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppIdleHistoryTests.java
@@ -16,18 +16,18 @@
package com.android.server.usage;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_SYSTEM;
+import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
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_RESTRICT_BACKGROUND_RESOURCE_USAGE;
import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
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 org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
import android.os.FileUtils;
import android.test.AndroidTestCase;
@@ -37,10 +37,12 @@ public class AppIdleHistoryTests extends AndroidTestCase {
File mStorageDir;
- final static String PACKAGE_1 = "com.android.testpackage1";
- final static String PACKAGE_2 = "com.android.testpackage2";
+ private static final String PACKAGE_1 = "com.android.testpackage1";
+ private static final String PACKAGE_2 = "com.android.testpackage2";
+ private static final String PACKAGE_3 = "com.android.testpackage3";
+ private static final String PACKAGE_4 = "com.android.testpackage4";
- final static int USER_ID = 0;
+ private static final int USER_ID = 0;
@Override
protected void setUp() throws Exception {
@@ -100,16 +102,27 @@ public class AppIdleHistoryTests extends AndroidTestCase {
aih.setAppStandbyBucket(PACKAGE_2, USER_ID, 2000, STANDBY_BUCKET_ACTIVE,
REASON_MAIN_USAGE);
+ aih.setAppStandbyBucket(PACKAGE_3, USER_ID, 2500, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_RESTRICT_BACKGROUND_RESOURCE_USAGE);
+ aih.setAppStandbyBucket(PACKAGE_4, USER_ID, 2750, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_USER);
aih.setAppStandbyBucket(PACKAGE_1, USER_ID, 3000, STANDBY_BUCKET_RARE,
REASON_MAIN_TIMEOUT);
assertEquals(aih.getAppStandbyBucket(PACKAGE_1, USER_ID, 3000), STANDBY_BUCKET_RARE);
assertEquals(aih.getAppStandbyBucket(PACKAGE_2, USER_ID, 3000), STANDBY_BUCKET_ACTIVE);
assertEquals(aih.getAppStandbyReason(PACKAGE_1, USER_ID, 3000), REASON_MAIN_TIMEOUT);
+ assertEquals(aih.getAppStandbyBucket(PACKAGE_3, USER_ID, 3000), STANDBY_BUCKET_RESTRICTED);
+ assertEquals(aih.getAppStandbyReason(PACKAGE_3, USER_ID, 3000),
+ REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_RESTRICT_BACKGROUND_RESOURCE_USAGE);
+ assertEquals(aih.getAppStandbyReason(PACKAGE_4, USER_ID, 3000),
+ REASON_MAIN_FORCED_BY_USER);
- // RARE is considered idle
+ // RARE and RESTRICTED are considered idle
assertTrue(aih.isIdle(PACKAGE_1, USER_ID, 3000));
assertFalse(aih.isIdle(PACKAGE_2, USER_ID, 3000));
+ assertTrue(aih.isIdle(PACKAGE_3, USER_ID, 3000));
+ assertTrue(aih.isIdle(PACKAGE_4, USER_ID, 3000));
// Check persistence
aih.writeAppIdleDurations();
@@ -118,6 +131,11 @@ public class AppIdleHistoryTests extends AndroidTestCase {
assertEquals(aih.getAppStandbyBucket(PACKAGE_1, USER_ID, 5000), STANDBY_BUCKET_RARE);
assertEquals(aih.getAppStandbyBucket(PACKAGE_2, USER_ID, 5000), STANDBY_BUCKET_ACTIVE);
assertEquals(aih.getAppStandbyReason(PACKAGE_1, USER_ID, 5000), REASON_MAIN_TIMEOUT);
+ assertEquals(aih.getAppStandbyBucket(PACKAGE_3, USER_ID, 3000), STANDBY_BUCKET_RESTRICTED);
+ assertEquals(aih.getAppStandbyReason(PACKAGE_3, USER_ID, 3000),
+ REASON_MAIN_FORCED_BY_SYSTEM | REASON_SUB_RESTRICT_BACKGROUND_RESOURCE_USAGE);
+ assertEquals(aih.getAppStandbyReason(PACKAGE_4, USER_ID, 3000),
+ REASON_MAIN_FORCED_BY_USER);
assertTrue(aih.shouldInformListeners(PACKAGE_1, USER_ID, 5000, STANDBY_BUCKET_RARE));
assertFalse(aih.shouldInformListeners(PACKAGE_1, USER_ID, 5000, STANDBY_BUCKET_RARE));
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 6aca58f400b3..03dc21370e24 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -28,11 +28,17 @@ import static android.app.usage.UsageStatsManager.REASON_MAIN_FORCED_BY_USER;
import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED;
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_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE;
+import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_MOVE_TO_FOREGROUND;
+import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYNC_ADAPTER;
+import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_SYSTEM_INTERACTION;
+import static android.app.usage.UsageStatsManager.REASON_SUB_USAGE_USER_INTERACTION;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
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 org.junit.Assert.assertEquals;
@@ -46,6 +52,8 @@ import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
import android.app.usage.AppStandbyInfo;
import android.app.usage.UsageEvents;
import android.appwidget.AppWidgetManager;
@@ -124,6 +132,13 @@ public class AppStandbyControllerTests {
public PackageManager getPackageManager() {
return mockPm;
}
+
+ public Object getSystemService(@NonNull String name) {
+ if (Context.ACTIVITY_SERVICE.equals(name)) {
+ return mock(ActivityManager.class);
+ }
+ return super.getSystemService(name);
+ }
}
static class MyInjector extends AppStandbyController.Injector {
@@ -253,8 +268,11 @@ public class AppStandbyControllerTests {
doReturn(packages).when(mockPm).getInstalledPackagesAsUser(anyInt(), anyInt());
try {
+ doReturn(UID_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_1), anyInt());
doReturn(UID_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_1), anyInt(), anyInt());
doReturn(UID_EXEMPTED_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_EXEMPTED_1),
+ anyInt());
+ doReturn(UID_EXEMPTED_1).when(mockPm).getPackageUidAsUser(eq(PACKAGE_EXEMPTED_1),
anyInt(), anyInt());
doReturn(pi.applicationInfo).when(mockPm).getApplicationInfo(eq(pi.packageName),
anyInt());
@@ -468,7 +486,7 @@ public class AppStandbyControllerTests {
}
@Test
- public void testPredictionTimedout() throws Exception {
+ public void testPredictionTimedOut() throws Exception {
// Set it to timeout or usage, so that prediction can override it
mInjector.mElapsedRealtime = HOUR_MS;
mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
@@ -532,6 +550,79 @@ public class AppStandbyControllerTests {
mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_NEVER,
REASON_MAIN_PREDICTED);
assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+
+ // Prediction can't remove from RESTRICTED
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_USER);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_PREDICTED);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_PREDICTED);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+
+ // Force from user can remove from RESTRICTED
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_USER);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_FORCED_BY_USER);
+ assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_FORCED_BY_USER);
+ assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+
+ // Force from system can remove from RESTRICTED if it was put it in due to system
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_USER);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_PREDICTED);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+
+ // Non-user usage can't remove from RESTRICTED
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_USAGE);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_USAGE | REASON_SUB_USAGE_SYSTEM_INTERACTION);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_USAGE | REASON_SUB_USAGE_SYNC_ADAPTER);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_USAGE | REASON_SUB_USAGE_EXEMPTED_SYNC_SCHEDULED_NON_DOZE);
+ assertEquals(STANDBY_BUCKET_RESTRICTED, getStandbyBucket(mController, PACKAGE_1));
+
+ // Explicit user usage can remove from RESTRICTED
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_USER);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
+ REASON_MAIN_USAGE | REASON_SUB_USAGE_USER_INTERACTION);
+ assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
+ REASON_MAIN_USAGE | REASON_SUB_USAGE_MOVE_TO_FOREGROUND);
+ assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
}
@Test
@@ -556,6 +647,55 @@ public class AppStandbyControllerTests {
assertBucket(STANDBY_BUCKET_RARE);
}
+ /**
+ * Test that setAppStandbyBucket to RESTRICTED doesn't change the bucket until the usage
+ * timeout has passed.
+ */
+ @Test
+ public void testTimeoutBeforeRestricted() throws Exception {
+ reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
+ assertBucket(STANDBY_BUCKET_ACTIVE);
+
+ mInjector.mElapsedRealtime += WORKING_SET_THRESHOLD;
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ // Bucket shouldn't change
+ assertBucket(STANDBY_BUCKET_ACTIVE);
+
+ // bucketing works after timeout
+ mInjector.mElapsedRealtime += DAY_MS;
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ assertBucket(STANDBY_BUCKET_RESTRICTED);
+
+ // Way past all timeouts. Make sure timeout processing doesn't raise bucket.
+ mInjector.mElapsedRealtime += RARE_THRESHOLD * 4;
+ mController.checkIdleStates(USER_ID);
+ assertBucket(STANDBY_BUCKET_RESTRICTED);
+ }
+
+ /**
+ * Test that an app is put into the RESTRICTED bucket after enough time has passed.
+ */
+ @Test
+ public void testRestrictedDelay() throws Exception {
+ reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
+ assertBucket(STANDBY_BUCKET_ACTIVE);
+
+ mInjector.mElapsedRealtime += mInjector.getRestrictedBucketDelayMs() - 5000;
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RESTRICTED,
+ REASON_MAIN_FORCED_BY_SYSTEM);
+ // Bucket shouldn't change
+ assertBucket(STANDBY_BUCKET_ACTIVE);
+
+ // bucketing works after timeout
+ mInjector.mElapsedRealtime += 6000;
+
+ Thread.sleep(6000);
+ // Enough time has passed. The app should automatically be put into the RESTRICTED bucket.
+ assertBucket(STANDBY_BUCKET_RESTRICTED);
+ }
+
@Test
public void testCascadingTimeouts() throws Exception {
reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);