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

Commit 728943fd authored by Kweku Adams's avatar Kweku Adams
Browse files

Don't give RESTRICTED jobs temp allowlist exemption.

Decoupling high priority cloud messages from standby buckets means
that it's now possible for WORKING_SET, RARE, and RESTRICTED apps
to send a large number of (or frequent) messages to the device.
High priority cloud messages often end up putting the app on the
temp allowlist. Since we give free EJ execution to apps on the
temp allowlist, this creates an avenue for apps to essentially get
unlimited/free EJ execution. Apps are intended to only be in the
RESTRICTED bucket for egregious cases, so it makes sense to not
give them free/unlimited EJ execution.

Bug: 228182860
Test: atest FrameworksMockingServicesTests:QuotaControllerTest
Change-Id: I31c1bd007412f0dc2b1b422c367e2c0af1213a11
parent 2cb1c268
Loading
Loading
Loading
Loading
+17 −9
Original line number Diff line number Diff line
@@ -307,7 +307,7 @@ public final class QuotaController extends StateController {
    private final SparseBooleanArray mTempAllowlistCache = new SparseBooleanArray();

    /**
     * Mapping of UIDs to the when their temp allowlist grace period ends (in the elapsed
     * Mapping of UIDs to when their temp allowlist grace period ends (in the elapsed
     * realtime timebase).
     */
    private final SparseLongArray mTempAllowlistGraceCache = new SparseLongArray();
@@ -815,6 +815,19 @@ public final class QuotaController extends StateController {
                jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
    }

    private boolean hasTempAllowlistExemptionLocked(int sourceUid, int standbyBucket,
            long nowElapsed) {
        if (standbyBucket == RESTRICTED_INDEX || standbyBucket == NEVER_INDEX) {
            // Don't let RESTRICTED apps get free quota from the temp allowlist.
            // TODO: consider granting the exemption to RESTRICTED apps if the temp allowlist allows
            // them to start FGS
            return false;
        }
        final long tempAllowlistGracePeriodEndElapsed = mTempAllowlistGraceCache.get(sourceUid);
        return mTempAllowlistCache.get(sourceUid)
                || nowElapsed < tempAllowlistGracePeriodEndElapsed;
    }

    /** @return true if the job is within expedited job quota. */
    @GuardedBy("mLock")
    public boolean isWithinEJQuotaLocked(@NonNull final JobStatus jobStatus) {
@@ -833,11 +846,8 @@ public final class QuotaController extends StateController {
        }

        final long nowElapsed = sElapsedRealtimeClock.millis();
        final long tempAllowlistGracePeriodEndElapsed =
                mTempAllowlistGraceCache.get(jobStatus.getSourceUid());
        final boolean hasTempAllowlistExemption = mTempAllowlistCache.get(jobStatus.getSourceUid())
                || nowElapsed < tempAllowlistGracePeriodEndElapsed;
        if (hasTempAllowlistExemption) {
        if (hasTempAllowlistExemptionLocked(jobStatus.getSourceUid(),
                jobStatus.getEffectiveStandbyBucket(), nowElapsed)) {
            return true;
        }

@@ -2127,10 +2137,8 @@ public final class QuotaController extends StateController {
            final long nowElapsed = sElapsedRealtimeClock.millis();
            final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName,
                    mPkg.userId, nowElapsed);
            final long tempAllowlistGracePeriodEndElapsed = mTempAllowlistGraceCache.get(mUid);
            final boolean hasTempAllowlistExemption = !mRegularJobTimer
                    && (mTempAllowlistCache.get(mUid)
                    || nowElapsed < tempAllowlistGracePeriodEndElapsed);
                    && hasTempAllowlistExemptionLocked(mUid, standbyBucket, nowElapsed);
            final long topAppGracePeriodEndElapsed = mTopAppGraceCache.get(mUid);
            final boolean hasTopAppExemption = !mRegularJobTimer
                    && (mTopAppCache.get(mUid) || nowElapsed < topAppGracePeriodEndElapsed);
+148 −0
Original line number Diff line number Diff line
@@ -2164,6 +2164,49 @@ public class QuotaControllerTest {
        }
    }

    @Test
    public void testIsWithinEJQuotaLocked_TempAllowlisting_Restricted() {
        setDischarging();
        JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_TempAllowlisting_Restricted", 1);
        setStandbyBucket(RESTRICTED_INDEX, js);
        setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_FREQUENT_MS, 10 * MINUTE_IN_MILLIS);
        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), true);
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (30 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), true);
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5), true);
        synchronized (mQuotaController.mLock) {
            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
        }

        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
        final long gracePeriodMs = 15 * SECOND_IN_MILLIS;
        setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, gracePeriodMs);
        Handler handler = mQuotaController.getHandler();
        spyOn(handler);

        // The temp allowlist should not enable RESTRICTED apps' to schedule & start EJs if they're
        // out of quota.
        mTempAllowlistListener.onAppAdded(mSourceUid);
        synchronized (mQuotaController.mLock) {
            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
        }
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mTempAllowlistListener.onAppRemoved(mSourceUid);
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        // Still in grace period
        synchronized (mQuotaController.mLock) {
            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
        }
        advanceElapsedClock(6 * SECOND_IN_MILLIS);
        // Out of grace period.
        synchronized (mQuotaController.mLock) {
            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
        }
    }

    /**
     * Tests that Timers properly track sessions when an app becomes top and is closed.
     */
@@ -5559,6 +5602,111 @@ public class QuotaControllerTest {
                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    @Test
    public void testEJTimerTracking_TempAllowlisting_Restricted() {
        setDischarging();
        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
        final long gracePeriodMs = 15 * SECOND_IN_MILLIS;
        setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, gracePeriodMs);
        Handler handler = mQuotaController.getHandler();
        spyOn(handler);

        JobStatus job = createExpeditedJobStatus("testEJTimerTracking_TempAllowlisting_Restricted", 1);
        setStandbyBucket(RESTRICTED_INDEX, job);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStartTrackingJobLocked(job, null);
        }
        assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
        List<TimingSession> expected = new ArrayList<>();

        long start = JobSchedulerService.sElapsedRealtimeClock.millis();
        synchronized (mQuotaController.mLock) {
            mQuotaController.prepareForExecutionLocked(job);
        }
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
        }
        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
        assertEquals(expected,
                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        advanceElapsedClock(SECOND_IN_MILLIS);

        // Job starts after app is added to temp allowlist and stops before removal.
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mTempAllowlistListener.onAppAdded(mSourceUid);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStartTrackingJobLocked(job, null);
            mQuotaController.prepareForExecutionLocked(job);
        }
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
        }
        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
        assertEquals(expected,
                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        // Job starts after app is added to temp allowlist and stops after removal,
        // before grace period ends.
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        mTempAllowlistListener.onAppAdded(mSourceUid);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStartTrackingJobLocked(job, null);
            mQuotaController.prepareForExecutionLocked(job);
        }
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        mTempAllowlistListener.onAppRemoved(mSourceUid);
        long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS;
        advanceElapsedClock(elapsedGracePeriodMs);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
        }
        expected.add(createTimingSession(start, 12 * SECOND_IN_MILLIS, 1));
        assertEquals(expected,
                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        advanceElapsedClock(SECOND_IN_MILLIS);
        elapsedGracePeriodMs += SECOND_IN_MILLIS;

        // Job starts during grace period and ends after grace period ends
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStartTrackingJobLocked(job, null);
            mQuotaController.prepareForExecutionLocked(job);
        }
        final long remainingGracePeriod = gracePeriodMs - elapsedGracePeriodMs;
        advanceElapsedClock(remainingGracePeriod);
        // Wait for handler to update Timer
        // Can't directly evaluate the message because for some reason, the captured message returns
        // the wrong 'what' even though the correct message goes to the handler and the correct
        // path executes.
        verify(handler, timeout(gracePeriodMs + 5 * SECOND_IN_MILLIS)).handleMessage(any());
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS + remainingGracePeriod, 1));
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
        }
        assertEquals(expected,
                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));

        // Job starts and runs completely after temp allowlist grace period.
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        start = JobSchedulerService.sElapsedRealtimeClock.millis();
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStartTrackingJobLocked(job, null);
            mQuotaController.prepareForExecutionLocked(job);
        }
        advanceElapsedClock(10 * SECOND_IN_MILLIS);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
        }
        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
        assertEquals(expected,
                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
    }

    /**
     * Tests that Timers properly track sessions when TOP state and temp allowlisting overlaps.
     */