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

Commit bc6275a5 authored by Xin Guan's avatar Xin Guan
Browse files

JobScheduler: Enable quota optimization overrides

Add new application compatibility override that can
control the jobscheduler quota enforcement to jobs running in
the foreground states or jobs started when apps in TOP state

The overrides only take effective if the quota optimization is
enabled.

Bug: 378129159
Test: FrameworksMockingServicesTests:com.android.server.job.controllers.QuotaControllerTest
Test: manual test.
Flag: EXEMPTED bug fix
Change-Id: I968b000cce5e8234fce743d666bc1000e9fffeeb
parent d6e830bb
Loading
Loading
Loading
Loading
+46 −7
Original line number Diff line number Diff line
@@ -36,10 +36,14 @@ import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.UidObserver;
import android.app.compat.CompatChanges;
import android.app.job.JobInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.UsageEventListener;
import android.compat.annotation.ChangeId;
import android.compat.annotation.Disabled;
import android.compat.annotation.Overridable;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
@@ -132,6 +136,27 @@ public final class QuotaController extends StateController {
        return (int) (val ^ (val >>> 32));
    }

    /**
     * When enabled this change id overrides the default quota policy enforcement to the jobs
     * running in the foreground process state.
     */
    // TODO: b/379681266 - Might need some refactoring for a better app-compat strategy.
    @VisibleForTesting
    @ChangeId
    @Disabled // Disabled by default
    @Overridable // The change can be overridden in user build
    static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS = 341201311L;

    /**
     * When enabled this change id overrides the default quota policy enforcement policy
     * the jobs started when app was in the TOP state.
     */
    @VisibleForTesting
    @ChangeId
    @Disabled // Disabled by default
    @Overridable // The change can be overridden in user build.
    static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS = 374323858L;

    @VisibleForTesting
    static class ExecutionStats {
        /**
@@ -622,7 +647,9 @@ public final class QuotaController extends StateController {
        }

        final int uid = jobStatus.getSourceUid();
        if (!Flags.enforceQuotaPolicyToTopStartedJobs() && mTopAppCache.get(uid)) {
        if ((!Flags.enforceQuotaPolicyToTopStartedJobs()
                || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
                        uid)) && mTopAppCache.get(uid)) {
            if (DEBUG) {
                Slog.d(TAG, jobStatus.toShortString() + " is top started job");
            }
@@ -659,7 +686,9 @@ public final class QuotaController extends StateController {
                timer.stopTrackingJob(jobStatus);
            }
        }
        if (!Flags.enforceQuotaPolicyToTopStartedJobs()) {
        if (!Flags.enforceQuotaPolicyToTopStartedJobs()
                || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
                        jobStatus.getSourceUid())) {
            mTopStartedJobs.remove(jobStatus);
        }
    }
@@ -772,7 +801,13 @@ public final class QuotaController extends StateController {

    /** @return true if the job was started while the app was in the TOP state. */
    private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
        return !Flags.enforceQuotaPolicyToTopStartedJobs() && mTopStartedJobs.contains(jobStatus);
        if (!Flags.enforceQuotaPolicyToTopStartedJobs()
                || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
                        jobStatus.getSourceUid())) {
            return mTopStartedJobs.contains(jobStatus);
        }

        return false;
    }

    /** Returns the maximum amount of time this job could run for. */
@@ -2634,9 +2669,13 @@ public final class QuotaController extends StateController {
    }

    @VisibleForTesting
    int getProcessStateQuotaFreeThreshold() {
        return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP :
                ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
    int getProcessStateQuotaFreeThreshold(int uid) {
        if (Flags.enforceQuotaPolicyToFgsJobs()
                && !CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS, uid)) {
            return ActivityManager.PROCESS_STATE_BOUND_TOP;
        }

        return ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
    }

    private class QcHandler extends Handler {
@@ -2776,7 +2815,7 @@ public final class QuotaController extends StateController {
                                isQuotaFree = true;
                            } else {
                                final boolean reprocess;
                                if (procState <= getProcessStateQuotaFreeThreshold()) {
                                if (procState <= getProcessStateQuotaFreeThreshold(uid)) {
                                    reprocess = !mForegroundUids.get(uid);
                                    mForegroundUids.put(uid, true);
                                    isQuotaFree = true;
+103 −1
Original line number Diff line number Diff line
@@ -65,6 +65,7 @@ import android.app.job.JobInfo;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -103,10 +104,13 @@ import com.android.server.job.controllers.QuotaController.TimedEvent;
import com.android.server.job.controllers.QuotaController.TimingSession;
import com.android.server.usage.AppStandbyInternal;

import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
@@ -135,6 +139,9 @@ public class QuotaControllerTest {
    private static final int SOURCE_USER_ID = 0;

    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Rule
    public TestRule compatChangeRule = new PlatformCompatChangeRule();
    private QuotaController mQuotaController;
    private QuotaController.QcConstants mQcConstants;
    private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
@@ -303,7 +310,7 @@ public class QuotaControllerTest {

    private int getProcessStateQuotaFreeThreshold() {
        synchronized (mQuotaController.mLock) {
            return mQuotaController.getProcessStateQuotaFreeThreshold();
            return mQuotaController.getProcessStateQuotaFreeThreshold(mSourceUid);
        }
    }

@@ -5197,6 +5204,101 @@ public class QuotaControllerTest {
        assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
    }

    @Test
    @EnableCompatChanges({QuotaController.OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS,
            QuotaController.OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS})
    @RequiresFlagsEnabled({Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS,
            Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS})
    public void testTracking_OutOfQuota_ForegroundAndBackground_CompactChangeOverrides() {
        setDischarging();

        JobStatus jobBg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 1);
        JobStatus jobTop = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 2);
        trackJobs(jobBg, jobTop);
        setStandbyBucket(WORKING_INDEX, jobTop, jobBg); // 2 hour window
        // Now the package only has 20 seconds to run.
        final long remainingTimeMs = 20 * SECOND_IN_MILLIS;
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(
                        JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
                        10 * MINUTE_IN_MILLIS - remainingTimeMs, 1), false);

        InOrder inOrder = inOrder(mJobSchedulerService);

        // UID starts out inactive.
        setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
        // Start the job.
        synchronized (mQuotaController.mLock) {
            mQuotaController.prepareForExecutionLocked(jobBg);
        }
        advanceElapsedClock(remainingTimeMs / 2);
        // New job starts after UID is in the foreground. Since the app is now in the foreground, it
        // should continue to have remainingTimeMs / 2 time remaining.
        setProcessState(ActivityManager.PROCESS_STATE_TOP);
        synchronized (mQuotaController.mLock) {
            mQuotaController.prepareForExecutionLocked(jobTop);
        }
        advanceElapsedClock(remainingTimeMs);

        // Wait for some extra time to allow for job processing.
        inOrder.verify(mJobSchedulerService,
                        timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0))
                .onControllerStateChanged(argThat(jobs -> jobs.size() > 0));
        synchronized (mQuotaController.mLock) {
            assertEquals(remainingTimeMs / 2,
                    mQuotaController.getRemainingExecutionTimeLocked(jobBg));
            assertEquals(remainingTimeMs / 2,
                    mQuotaController.getRemainingExecutionTimeLocked(jobTop));
        }
        // Go to a background state.
        setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING);
        advanceElapsedClock(remainingTimeMs / 2 + 1);
        // Only Bg job will be changed from in-quota to out-of-quota.
        inOrder.verify(mJobSchedulerService,
                        timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1))
                .onControllerStateChanged(argThat(jobs -> jobs.size() == 1));
        // Top job should still be allowed to run.
        assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));

        // New jobs to run.
        JobStatus jobBg2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 3);
        JobStatus jobTop2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 4);
        JobStatus jobFg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 5);
        setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg);

        advanceElapsedClock(20 * SECOND_IN_MILLIS);
        setProcessState(ActivityManager.PROCESS_STATE_TOP);
        inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
                .onControllerStateChanged(argThat(jobs -> jobs.size() == 1));
        trackJobs(jobFg, jobTop);
        synchronized (mQuotaController.mLock) {
            mQuotaController.prepareForExecutionLocked(jobTop);
        }
        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));

        // App still in foreground so everything should be in quota.
        advanceElapsedClock(20 * SECOND_IN_MILLIS);
        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));

        advanceElapsedClock(20 * SECOND_IN_MILLIS);
        setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
        inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
                .onControllerStateChanged(argThat(jobs -> jobs.size() == 2));
        // App is now in background and out of quota. Fg should now change to out of quota since it
        // wasn't started. Top should remain in quota since it started when the app was in TOP.
        assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
        trackJobs(jobBg2);
        assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
    }

    /**
     * Tests that TOP jobs are stopped when an app runs out of quota.
     */