diff options
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 ≤ {@link #STANDBY_BUCKET_ACTIVE} have no standby restrictions imposed. * Apps in buckets > {@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); |