diff options
2 files changed, 165 insertions, 9 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index f26e051581f3..c6ba1eac56c0 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -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); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 9a4f8e261124..43ba39adcb85 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -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. */ |