Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bbbad9cc authored by Amith Yamasani's avatar Amith Yamasani
Browse files

Cascading timeouts for App Standby

Fixes overlapping of predictions, strong usage events
and mild usage events which have forced durations.

Having separate timeouts for ACTIVE and WORKING_SET
and moving between them when necessary prevents
getting stuck in the wrong state (higher) for longer
than necessary.

Bug: 73294677
Test: atest FrameworksServicesTests:AppStandbyControllerTests
Change-Id: I35530e62cffe2c86945b5da64a41704f807708ce
parent d591904d
Loading
Loading
Loading
Loading
+84 −0
Original line number Diff line number Diff line
@@ -308,6 +308,7 @@ public class AppStandbyControllerTests {
    private void reportEvent(AppStandbyController controller, int eventType,
            long elapsedTime) {
        // Back to ACTIVE on event
        mInjector.mElapsedRealtime = elapsedTime;
        UsageEvents.Event ev = new UsageEvents.Event();
        ev.mPackage = PACKAGE_1;
        ev.mEventType = eventType;
@@ -486,6 +487,89 @@ public class AppStandbyControllerTests {
        assertBucket(STANDBY_BUCKET_FREQUENT);
    }

    @Test
    public void testCascadingTimeouts() throws Exception {
        setChargingState(mController, false);

        reportEvent(mController, USER_INTERACTION, 0);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        reportEvent(mController, NOTIFICATION_SEEN, 1000);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_WORKING_SET,
                REASON_PREDICTED, 1000);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                REASON_PREDICTED, 2000 + mController.mStrongUsageTimeoutMillis);
        assertBucket(STANDBY_BUCKET_WORKING_SET);

        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                REASON_PREDICTED, 2000 + mController.mNotificationSeenTimeoutMillis);
        assertBucket(STANDBY_BUCKET_FREQUENT);
    }

    @Test
    public void testOverlappingTimeouts() throws Exception {
        setChargingState(mController, false);

        reportEvent(mController, USER_INTERACTION, 0);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        reportEvent(mController, NOTIFICATION_SEEN, 1000);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        // Overlapping USER_INTERACTION before previous one times out
        reportEvent(mController, USER_INTERACTION, mController.mStrongUsageTimeoutMillis - 1000);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        // Still in ACTIVE after first USER_INTERACTION times out
        mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis + 1000;
        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                REASON_PREDICTED, mInjector.mElapsedRealtime);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        // Both timed out, so NOTIFICATION_SEEN timeout should be effective
        mInjector.mElapsedRealtime = mController.mStrongUsageTimeoutMillis * 2 + 2000;
        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_FREQUENT,
                REASON_PREDICTED, mInjector.mElapsedRealtime);
        assertBucket(STANDBY_BUCKET_WORKING_SET);

        mInjector.mElapsedRealtime = mController.mNotificationSeenTimeoutMillis + 2000;
        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
                REASON_PREDICTED, mInjector.mElapsedRealtime);
        assertBucket(STANDBY_BUCKET_RARE);
    }

    @Test
    public void testPredictionNotOverridden() throws Exception {
        setChargingState(mController, false);

        reportEvent(mController, USER_INTERACTION, 0);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        mInjector.mElapsedRealtime = WORKING_SET_THRESHOLD - 1000;
        reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        // Falls back to WORKING_SET
        mInjector.mElapsedRealtime += 5000;
        mController.checkIdleStates(USER_ID);
        assertBucket(STANDBY_BUCKET_WORKING_SET);

        // Predict to ACTIVE
        mInjector.mElapsedRealtime += 1000;
        mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_ACTIVE,
                REASON_PREDICTED, mInjector.mElapsedRealtime);
        assertBucket(STANDBY_BUCKET_ACTIVE);

        // CheckIdleStates should not change the prediction
        mInjector.mElapsedRealtime += 1000;
        mController.checkIdleStates(USER_ID);
        assertBucket(STANDBY_BUCKET_ACTIVE);
    }

    @Test
    public void testAddActiveDeviceAdmin() {
        assertActiveAdmins(USER_ID, (String[]) null);
+48 −18
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static android.app.usage.UsageStatsManager.REASON_USAGE;
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_WORKING_SET;

import android.app.usage.UsageStatsManager;
import android.os.SystemClock;
@@ -87,7 +88,9 @@ public class AppIdleHistory {
    // The last time a job was run for this app
    private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime";
    // The time when the forced active state can be overridden.
    private static final String ATTR_BUCKET_TIMEOUT_TIME = "bucketTimeoutTime";
    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";

    // device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
    private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
@@ -117,11 +120,15 @@ public class AppIdleHistory {
        int lastInformedBucket;
        // The last time a job was run for this app, using elapsed timebase
        long lastJobRunTime;
        // When should the bucket state timeout, in elapsed timebase, if greater than
        // When should the bucket active state timeout, in elapsed timebase, if greater than
        // lastUsedElapsedTime.
        // This is used to keep the app in a high bucket regardless of other timeouts and
        // predictions.
        long bucketTimeoutTime;
        long bucketActiveTimeoutTime;
        // If there's a forced working_set state, this is when it times out. This can be sitting
        // under any active state timeout, so that it becomes applicable after the active state
        // timeout expires.
        long bucketWorkingSetTimeoutTime;
    }

    AppIdleHistory(File storageDir, long elapsedRealtime) {
@@ -208,11 +215,28 @@ public class AppIdleHistory {
     * @param packageName name of the app being updated, for logging purposes
     * @param newBucket the bucket to set the app to
     * @param elapsedRealtime mark as used time if non-zero
     * @param timeout set the timeout of the specified bucket, if non-zero
     * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used
     *                with bucket values of ACTIVE and WORKING_SET.
     * @return
     */
    public AppUsageHistory reportUsage(AppUsageHistory appUsageHistory, String packageName,
            int newBucket, 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);
            }
        }

        if (elapsedRealtime != 0) {
            appUsageHistory.lastUsedElapsedTime = mElapsedDuration
                    + (elapsedRealtime - mElapsedSnapshot);
@@ -226,12 +250,6 @@ public class AppIdleHistory {
                        .currentBucket
                        + ", reason=" + appUsageHistory.bucketingReason);
            }
            if (timeout > elapsedRealtime) {
                // Convert to elapsed timebase
                appUsageHistory.bucketTimeoutTime =
                        Math.max(appUsageHistory.bucketTimeoutTime,
                                mElapsedDuration + (timeout - mElapsedSnapshot));
            }
        }
        appUsageHistory.bucketingReason = REASON_USAGE;

@@ -247,7 +265,8 @@ public class AppIdleHistory {
     * @param userId
     * @param newBucket the bucket to set the app to
     * @param elapsedRealtime mark as used time if non-zero
     * @param timeout set the timeout of the specified bucket, if non-zero
     * @param timeout set the timeout of the specified bucket, if non-zero. Can only be used
     *                with bucket values of ACTIVE and WORKING_SET.
     * @return
     */
    public AppUsageHistory reportUsage(String packageName, int userId, int newBucket,
@@ -504,8 +523,10 @@ public class AppIdleHistory {
                                parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
                        appUsageHistory.lastJobRunTime = getLongValue(parser,
                                ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE);
                        appUsageHistory.bucketTimeoutTime = getLongValue(parser,
                                ATTR_BUCKET_TIMEOUT_TIME, 0L);
                        appUsageHistory.bucketActiveTimeoutTime = getLongValue(parser,
                                ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, 0L);
                        appUsageHistory.bucketWorkingSetTimeoutTime = getLongValue(parser,
                                ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, 0L);
                        if (appUsageHistory.bucketingReason == null) {
                            appUsageHistory.bucketingReason = REASON_DEFAULT;
                        }
@@ -557,9 +578,13 @@ public class AppIdleHistory {
                xml.attribute(null, ATTR_CURRENT_BUCKET,
                        Integer.toString(history.currentBucket));
                xml.attribute(null, ATTR_BUCKETING_REASON, history.bucketingReason);
                if (history.bucketTimeoutTime > 0) {
                    xml.attribute(null, ATTR_BUCKET_TIMEOUT_TIME, Long.toString(history
                            .bucketTimeoutTime));
                if (history.bucketActiveTimeoutTime > 0) {
                    xml.attribute(null, ATTR_BUCKET_ACTIVE_TIMEOUT_TIME, Long.toString(history
                            .bucketActiveTimeoutTime));
                }
                if (history.bucketWorkingSetTimeoutTime > 0) {
                    xml.attribute(null, ATTR_BUCKET_WORKING_SET_TIMEOUT_TIME, Long.toString(history
                            .bucketWorkingSetTimeoutTime));
                }
                if (history.lastJobRunTime != Long.MIN_VALUE) {
                    xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history
@@ -593,14 +618,19 @@ public class AppIdleHistory {
                continue;
            }
            idpw.print("package=" + packageName);
            idpw.print(" userId=" + userId);
            idpw.print(" lastUsedElapsed=");
            TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw);
            idpw.print(" lastUsedScreenOn=");
            TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
            idpw.print(" lastPredictedTime=");
            TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw);
            idpw.print(" bucketTimeoutTime=");
            TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.bucketTimeoutTime, idpw);
            idpw.print(" bucketActiveTimeoutTime=");
            TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.bucketActiveTimeoutTime,
                    idpw);
            idpw.print(" bucketWorkingSetTimeoutTime=");
            TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.bucketWorkingSetTimeoutTime,
                    idpw);
            idpw.print(" lastJobRunTime=");
            TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
            idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
+124 −55
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;

import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.usage.UsageStatsManager.StandbyBuckets;
@@ -171,6 +172,8 @@ public class AppStandbyController {
    static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8;
    static final int MSG_PAROLE_STATE_CHANGED = 9;
    static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10;
    /** Check the state of one app: arg1 = userId, arg2 = uid, obj = (String) packageName */
    static final int MSG_CHECK_PACKAGE_IDLE_STATE = 11;

    long mCheckIdleIntervalMillis;
    long mAppIdleParoleIntervalMillis;
@@ -322,7 +325,7 @@ public class AppStandbyController {
        // Get sync adapters for the authority
        String[] packages = ContentResolver.getSyncAdapterPackagesForAuthorityAsUser(
                authority, userId);
        final long elapsedRealtime = SystemClock.elapsedRealtime();
        final long elapsedRealtime = mInjector.elapsedRealtime();
        for (String packageName: packages) {
            // Only force the sync adapters to active if the provider is not in the same package and
            // the sync adapter is a system package.
@@ -460,8 +463,31 @@ public class AppStandbyController {
            for (int p = 0; p < packageCount; p++) {
                final PackageInfo pi = packages.get(p);
                final String packageName = pi.packageName;
                checkAndUpdateStandbyState(packageName, userId, pi.applicationInfo.uid,
                        elapsedRealtime);
            }
        }
        if (DEBUG) {
            Slog.d(TAG, "checkIdleStates took "
                    + (mInjector.elapsedRealtime() - elapsedRealtime));
        }
        return true;
    }

    /** Check if we need to update the standby state of a specific app. */
    private void checkAndUpdateStandbyState(String packageName, @UserIdInt int userId,
            int uid, long elapsedRealtime) {
        if (uid <= 0) {
            try {
                uid = mPackageManager.getPackageUidAsUser(packageName, userId);
            } catch (PackageManager.NameNotFoundException e) {
                // Not a valid package for this user, nothing to do
                // TODO: Remove any history of removed packages
                return;
            }
        }
        final boolean isSpecial = isAppSpecial(packageName,
                        UserHandle.getAppId(pi.applicationInfo.uid),
                UserHandle.getAppId(uid),
                userId);
        if (DEBUG) {
            Slog.d(TAG, "   Checking idle state for " + packageName + " special=" +
@@ -476,45 +502,61 @@ public class AppStandbyController {
                    STANDBY_BUCKET_EXEMPTED, false);
        } else {
            synchronized (mAppIdleLock) {
                        AppIdleHistory.AppUsageHistory app =
                final AppIdleHistory.AppUsageHistory app =
                        mAppIdleHistory.getAppUsageHistory(packageName,
                        userId, elapsedRealtime);
                        // If the bucket was forced by the developer or the app is within the
                        // temporary active period, leave it alone.
                        if (REASON_FORCED.equals(app.bucketingReason)
                                || !hasBucketTimeoutPassed(app, elapsedRealtime)) {
                            continue;
                String reason = app.bucketingReason;

                // If the bucket was forced by the user/developer, leave it alone.
                // A usage event will be the only way to bring it out of this forced state
                if (REASON_FORCED.equals(app.bucketingReason)) {
                    return;
                }
                final int oldBucket = app.currentBucket;
                int newBucket = Math.max(oldBucket, STANDBY_BUCKET_ACTIVE); // Undo EXEMPTED
                boolean predictionLate = false;
                        // If the bucket was moved up due to usage, let the timeouts apply.
                // Compute age-based bucket
                if (REASON_DEFAULT.equals(app.bucketingReason)
                        || REASON_USAGE.equals(app.bucketingReason)
                        || REASON_TIMEOUT.equals(app.bucketingReason)
                        || (predictionLate = predictionTimedOut(app, elapsedRealtime))) {
                            int oldBucket = app.currentBucket;
                            int newBucket = getBucketForLocked(packageName, userId,
                    newBucket = getBucketForLocked(packageName, userId,
                            elapsedRealtime);
                    if (DEBUG) {
                        Slog.d(TAG, "Evaluated AOSP newBucket = " + newBucket);
                    }
                    reason = REASON_TIMEOUT;
                }
                // Check if the app is within one of the timeouts for forced bucket elevation
                final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime);
                if (newBucket >= STANDBY_BUCKET_ACTIVE
                        && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) {
                    newBucket = STANDBY_BUCKET_ACTIVE;
                    reason = REASON_USAGE;
                    if (DEBUG) {
                        Slog.d(TAG, "    Keeping at ACTIVE due to min timeout");
                    }
                } else if (newBucket >= STANDBY_BUCKET_WORKING_SET
                        && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) {
                    newBucket = STANDBY_BUCKET_WORKING_SET;
                    reason = REASON_USAGE;
                    if (DEBUG) {
                        Slog.d(TAG, "    Keeping at WORKING_SET due to min timeout");
                    }
                }
                if (DEBUG) {
                    Slog.d(TAG, "     Old bucket=" + oldBucket
                            + ", newBucket=" + newBucket);
                }
                if (oldBucket < newBucket || predictionLate) {
                    mAppIdleHistory.setAppStandbyBucket(packageName, userId,
                                        elapsedRealtime, newBucket, REASON_TIMEOUT);
                            elapsedRealtime, newBucket, reason);
                    maybeInformListeners(packageName, userId, elapsedRealtime,
                            newBucket, false);
                }
            }
        }
    }
            }
        }
        if (DEBUG) {
            Slog.d(TAG, "checkIdleStates took "
                    + (mInjector.elapsedRealtime() - elapsedRealtime));
        }
        return true;
    }

    private boolean predictionTimedOut(AppIdleHistory.AppUsageHistory app, long elapsedRealtime) {
        return app.bucketingReason != null
@@ -526,7 +568,9 @@ public class AppStandbyController {

    private boolean hasBucketTimeoutPassed(AppIdleHistory.AppUsageHistory app,
            long elapsedRealtime) {
        return app.bucketTimeoutTime < mAppIdleHistory.getElapsedTime(elapsedRealtime);
        final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime);
        return app.bucketActiveTimeoutTime < elapsedTimeAdjusted
                && app.bucketWorkingSetTimeoutTime < elapsedTimeAdjusted;
    }

    private void maybeInformListeners(String packageName, int userId,
@@ -631,16 +675,22 @@ public class AppStandbyController {
                        event.mPackage, userId, elapsedRealtime);
                final int prevBucket = appHistory.currentBucket;
                final String prevBucketReason = appHistory.bucketingReason;
                final long nextCheckTime;
                if (event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN) {
                    // Mild usage elevates to WORKING_SET but doesn't change usage time.
                    mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                            STANDBY_BUCKET_WORKING_SET,
                            elapsedRealtime, elapsedRealtime + mNotificationSeenTimeoutMillis);
                            0, elapsedRealtime + mNotificationSeenTimeoutMillis);
                    nextCheckTime = mNotificationSeenTimeoutMillis;
                } else {
                    mAppIdleHistory.reportUsage(event.mPackage, userId,
                    mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                            STANDBY_BUCKET_ACTIVE,
                            elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
                    nextCheckTime = mStrongUsageTimeoutMillis;
                }

                mHandler.sendMessageDelayed(mHandler.obtainMessage
                        (MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, event.mPackage),
                        nextCheckTime);
                final boolean userStartedInteracting =
                        appHistory.currentBucket == STANDBY_BUCKET_ACTIVE &&
                        prevBucket != appHistory.currentBucket &&
@@ -932,9 +982,24 @@ public class AppStandbyController {

            // 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 && app.currentBucket < newBucket
                    && !hasBucketTimeoutPassed(app, elapsedRealtime)) {
                return;
            if (predicted) {
                // Check if the app is within one of the timeouts for forced bucket elevation
                final long elapsedTimeAdjusted = mAppIdleHistory.getElapsedTime(elapsedRealtime);
                if (newBucket > STANDBY_BUCKET_ACTIVE
                        && app.bucketActiveTimeoutTime > elapsedTimeAdjusted) {
                    newBucket = STANDBY_BUCKET_ACTIVE;
                    reason = REASON_USAGE;
                    if (DEBUG) {
                        Slog.d(TAG, "    Keeping at ACTIVE due to min timeout");
                    }
                } else if (newBucket > STANDBY_BUCKET_WORKING_SET
                        && app.bucketWorkingSetTimeoutTime > elapsedTimeAdjusted) {
                    newBucket = STANDBY_BUCKET_WORKING_SET;
                    reason = REASON_USAGE;
                    if (DEBUG) {
                        Slog.d(TAG, "    Keeping at WORKING_SET due to min timeout");
                    }
                }
            }

            mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
@@ -1347,6 +1412,10 @@ public class AppStandbyController {
                            + ", Charging state:" + mCharging);
                    informParoleStateChanged();
                    break;
                case MSG_CHECK_PACKAGE_IDLE_STATE:
                    checkAndUpdateStandbyState((String) msg.obj, msg.arg1, msg.arg2,
                            mInjector.elapsedRealtime());
                    break;
                default:
                    super.handleMessage(msg);
                    break;