diff options
| author | 2020-01-08 18:37:26 -0800 | |
|---|---|---|
| committer | 2020-01-22 09:27:49 -0800 | |
| commit | c6a9b341e4b6b8c0bfc7da33d8caa8b6af959b39 (patch) | |
| tree | 0206719ec686eb253be83c3664d7b9feb3373f21 | |
| parent | c57d5c4bbb7662d52165195d9643125172d5e454 (diff) | |
Creating new RESTRICTED bucket.
This new bucket sits between RARE and NEVER and will have more severe
restrictions than RARE. Jobs, alarms, background services, and more will
be affected. For now, we just create the bucket. Changes to the various
subsystems will be done in separate CLs. This bucket is not meant to be
used if the user has used the app recently, so there are safeguards in
place to make sure that doesn't happen. A new system-internal API is
added so it's easier to track internal calls and so that other
subsystems can provide explicit reasons for forcing an app into the
bucket.
Bug: 145551233
Test: atest AppIdleHistoryTests
Test: atest AppStandbyControllerTests
Test: atest UsageStatsTest
Change-Id: I8ae2e6fd2353111f6bcc176a5a1929551db01e0f
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); |