Adding limit for active apps.
Add an overall time limit for all apps, including ACTIVE apps. Right
now, the default is 4 hours per day, so apps can only have their jobs
running for a maximum of 4 hours in a rolling 24 hour window.
Also fix calculation bug where an app could be brought back into quota
despite having less than the quota buffer time available.
Bug: 117846754
Bug: 111423978
Test: atest com.android.server.job.controllers.QuotaControllerTest
Change-Id: Ia0773ef9fe26f0a502fe487f1e11c243eede30b3
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 effb5a7..8bbcd6f 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
@@ -30,6 +30,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -62,6 +63,7 @@
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.controllers.QuotaController.ExecutionStats;
import com.android.server.job.controllers.QuotaController.TimingSession;
import org.junit.After;
@@ -131,13 +133,18 @@
doReturn(mock(PackageManagerInternal.class))
.when(() -> LocalServices.getService(PackageManagerInternal.class));
- // Freeze the clocks at this moment in time
+ // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
+ // in the past, and QuotaController sometimes floors values at 0, so if the test time
+ // causes sessions with negative timestamps, they will fail.
JobSchedulerService.sSystemClock =
- Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
- JobSchedulerService.sUptimeMillisClock =
- Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
- JobSchedulerService.sElapsedRealtimeClock =
- Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
+ getAdvancedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC),
+ 24 * HOUR_IN_MILLIS);
+ JobSchedulerService.sUptimeMillisClock = getAdvancedClock(
+ Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC),
+ 24 * HOUR_IN_MILLIS);
+ JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
+ Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC),
+ 24 * HOUR_IN_MILLIS);
// Initialize real objects.
// Capture the listeners.
@@ -291,9 +298,17 @@
mQuotaController.saveTimingSession(0, "com.android.test.stay", two);
mQuotaController.saveTimingSession(0, "com.android.test.stay", one);
+ ExecutionStats expectedStats = new ExecutionStats();
+ expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+
mQuotaController.onAppRemovedLocked("com.android.test.remove", 10001);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test.remove"));
assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test.stay"));
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test.remove", RARE_INDEX));
+ assertNotEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test.stay", RARE_INDEX));
}
@Test
@@ -318,13 +333,21 @@
mQuotaController.saveTimingSession(10, "com.android.test", two);
mQuotaController.saveTimingSession(10, "com.android.test", one);
+ ExecutionStats expectedStats = new ExecutionStats();
+ expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+
mQuotaController.onUserRemovedLocked(0);
assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
assertEquals(expected, mQuotaController.getTimingSessions(10, "com.android.test"));
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+ assertNotEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(10, "com.android.test", RARE_INDEX));
}
@Test
- public void testGetTrailingExecutionTimeLocked_NoTimer() {
+ public void testUpdateExecutionStatsLocked_NoTimer() {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
// Added in chronological order.
mQuotaController.saveTimingSession(0, "com.android.test",
@@ -340,32 +363,288 @@
mQuotaController.saveTimingSession(0, "com.android.test",
createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
- assertEquals(0, mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- MINUTE_IN_MILLIS));
- assertEquals(2 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 3 * MINUTE_IN_MILLIS));
- assertEquals(4 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 5 * MINUTE_IN_MILLIS));
- assertEquals(4 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 49 * MINUTE_IN_MILLIS));
- assertEquals(5 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 50 * MINUTE_IN_MILLIS));
- assertEquals(6 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- HOUR_IN_MILLIS));
- assertEquals(11 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 2 * HOUR_IN_MILLIS));
- assertEquals(12 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 3 * HOUR_IN_MILLIS));
- assertEquals(22 * MINUTE_IN_MILLIS,
- mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
- 6 * HOUR_IN_MILLIS));
+ // Test an app that hasn't had any activity.
+ ExecutionStats expectedStats = new ExecutionStats();
+ ExecutionStats inputStats = new ExecutionStats();
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 12 * HOUR_IN_MILLIS;
+ // Invalid time is now +24 hours since there are no sessions at all for the app.
+ expectedStats.invalidTimeElapsed = now + 24 * HOUR_IN_MILLIS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test.not.run", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = MINUTE_IN_MILLIS;
+ // Invalid time is now +18 hours since there are no sessions in the window but the earliest
+ // session is 6 hours ago.
+ expectedStats.invalidTimeElapsed = now + 18 * HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 0;
+ expectedStats.bgJobCountInWindow = 0;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS;
+ // Invalid time is now since the session straddles the window cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 2 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 3;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * MINUTE_IN_MILLIS;
+ // Invalid time is now since the start of the session is at the very edge of the window
+ // cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 3;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS;
+ // Invalid time is now +44 minutes since the earliest session in the window is now-5
+ // minutes.
+ expectedStats.invalidTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 4 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 3;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS;
+ // Invalid time is now since the session is at the very edge of the window cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 5 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 4;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
+ // Invalid time is now since the start of the session is at the very edge of the window
+ // cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 6 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 5;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ // Invalid time is now since the session straddles the window cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 11 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * HOUR_IN_MILLIS;
+ // Invalid time is now +59 minutes since the earliest session in the window is now-121
+ // minutes.
+ expectedStats.invalidTimeElapsed = now + 59 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 12 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS;
+ // Invalid time is now since the start of the session is at the very edge of the window
+ // cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 15;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ // Make sure invalidTimeElapsed is set correctly when it's dependent on the max period.
+ mQuotaController.getTimingSessions(0, "com.android.test")
+ .add(0,
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 3));
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ // Invalid time is now +1 hour since the earliest session in the max period is 1 hour
+ // before the end of the max period cutoff time.
+ expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 23 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 18;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+
+ mQuotaController.getTimingSessions(0, "com.android.test")
+ .add(0,
+ createTimingSession(now - (24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 2));
+ inputStats.windowSizeMs = expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ // Invalid time is now since the earlist session straddles the max period cutoff time.
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 22 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 24 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.updateExecutionStatsLocked(0, "com.android.test", inputStats);
+ assertEquals(expectedStats, inputStats);
+ }
+
+ /**
+ * Tests that getExecutionStatsLocked returns the correct stats.
+ */
+ @Test
+ public void testGetExecutionStatsLocked_Values() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+
+ ExecutionStats expectedStats = new ExecutionStats();
+
+ // Active
+ expectedStats.windowSizeMs = 10 * MINUTE_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now + 4 * MINUTE_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 5;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX));
+
+ // Working
+ expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now;
+ expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 10;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX));
+
+ // Frequent
+ expectedStats.windowSizeMs = 8 * HOUR_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 15;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", FREQUENT_INDEX));
+
+ // Rare
+ expectedStats.windowSizeMs = 24 * HOUR_IN_MILLIS;
+ expectedStats.invalidTimeElapsed = now + HOUR_IN_MILLIS;
+ expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInWindow = 20;
+ expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS;
+ expectedStats.bgJobCountInMaxPeriod = 20;
+ expectedStats.quotaCutoffTimeElapsed = now - (2 * HOUR_IN_MILLIS - 3 * MINUTE_IN_MILLIS)
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ assertEquals(expectedStats,
+ mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX));
+ }
+
+ /**
+ * Tests that getExecutionStatsLocked properly caches the stats and returns the cached object.
+ */
+ @Test
+ public void testGetExecutionStatsLocked_Caching() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
+ mQuotaController.saveTimingSession(0, "com.android.test",
+ createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5));
+ final ExecutionStats originalStatsActive = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", ACTIVE_INDEX);
+ final ExecutionStats originalStatsWorking = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", WORKING_INDEX);
+ final ExecutionStats originalStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", FREQUENT_INDEX);
+ final ExecutionStats originalStatsRare = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", RARE_INDEX);
+
+ // Advance clock so that the working stats shouldn't be the same.
+ advanceElapsedClock(MINUTE_IN_MILLIS);
+ // Change frequent bucket size so that the stats need to be recalculated.
+ mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 6 * HOUR_IN_MILLIS;
+ mQuotaController.onConstantsUpdatedLocked();
+
+ ExecutionStats expectedStats = new ExecutionStats();
+ expectedStats.windowSizeMs = originalStatsActive.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsActive.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsActive.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsActive.bgJobCountInWindow;
+ expectedStats.executionTimeInMaxPeriodMs = originalStatsActive.executionTimeInMaxPeriodMs;
+ expectedStats.bgJobCountInMaxPeriod = originalStatsActive.bgJobCountInMaxPeriod;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsActive.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsActive = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", ACTIVE_INDEX);
+ // Stats for the same bucket should use the same object.
+ assertTrue(originalStatsActive == newStatsActive);
+ assertEquals(expectedStats, newStatsActive);
+
+ expectedStats.windowSizeMs = originalStatsWorking.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsWorking.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsWorking.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsWorking.bgJobCountInWindow;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsWorking.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsWorking = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", WORKING_INDEX);
+ assertTrue(originalStatsWorking == newStatsWorking);
+ assertNotEquals(expectedStats, newStatsWorking);
+
+ expectedStats.windowSizeMs = originalStatsFrequent.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsFrequent.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsFrequent.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsFrequent.bgJobCountInWindow;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsFrequent.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsFrequent = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", FREQUENT_INDEX);
+ assertTrue(originalStatsFrequent == newStatsFrequent);
+ assertNotEquals(expectedStats, newStatsFrequent);
+
+ expectedStats.windowSizeMs = originalStatsRare.windowSizeMs;
+ expectedStats.invalidTimeElapsed = originalStatsRare.invalidTimeElapsed;
+ expectedStats.executionTimeInWindowMs = originalStatsRare.executionTimeInWindowMs;
+ expectedStats.bgJobCountInWindow = originalStatsRare.bgJobCountInWindow;
+ expectedStats.quotaCutoffTimeElapsed = originalStatsRare.quotaCutoffTimeElapsed;
+ final ExecutionStats newStatsRare = mQuotaController.getExecutionStatsLocked(0,
+ "com.android.test", RARE_INDEX);
+ assertTrue(originalStatsRare == newStatsRare);
+ assertEquals(expectedStats, newStatsRare);
}
@Test
@@ -394,6 +673,56 @@
}
@Test
+ public void testMaybeScheduleStartAlarmLocked_Active() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ // Active window size is 10 minutes.
+ final int standbyBucket = ACTIVE_INDEX;
+
+ // No sessions saved yet.
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Test with timing sessions out of window but still under max execution limit.
+ final long expectedAlarmTime =
+ (now - 18 * HOUR_IN_MILLIS) + 24 * HOUR_IN_MILLIS
+ + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 18 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 12 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 7 * HOUR_IN_MILLIS, HOUR_IN_MILLIS, 1));
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1));
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+
+ JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1);
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ advanceElapsedClock(5 * MINUTE_IN_MILLIS);
+ // Timer has only been going for 5 minutes in the past 10 minutes, which is under the window
+ // size limit, but the total execution time for the past 24 hours is 6 hours, so the job no
+ // longer has quota.
+ assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked(jobStatus));
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, times(1)).set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK),
+ any(), any());
+ }
+
+ @Test
public void testMaybeScheduleStartAlarmLocked_WorkingSet() {
// saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
// because it schedules an alarm too. Prevent it from doing so.
@@ -620,6 +949,124 @@
inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
}
+
+ /**
+ * Tests that the start alarm is properly rescheduled if the earliest session that contributes
+ * to the app being out of quota contributes less than the quota buffer time.
+ */
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_DefaultValues() {
+ // Use the default values
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedBufferSize() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedAllowedTime() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedMaxTime() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ @Test
+ public void testMaybeScheduleStartAlarmLocked_SmallRollingQuota_UpdatedEverything() {
+ // Make sure any new value is used correctly.
+ mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS *= 2;
+ mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS /= 2;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS /= 2;
+ mQuotaController.onConstantsUpdatedLocked();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck();
+ mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE).clear();
+ runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck();
+ }
+
+ private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_AllowedTimeCheck() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Working set window size is 2 hours.
+ final int standbyBucket = WORKING_INDEX;
+ final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2;
+ final long remainingTimeMs =
+ mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS - contributionMs;
+
+ // Session straddles edge of bucket window. Only the contribution should be counted towards
+ // the quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - HOUR_IN_MILLIS, remainingTimeMs, 2));
+ // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+ // is 2 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+ final long expectedAlarmTime = now - HOUR_IN_MILLIS + 2 * HOUR_IN_MILLIS
+ + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs);
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
+
+
+ private void runTestMaybeScheduleStartAlarmLocked_SmallRollingQuota_MaxTimeCheck() {
+ // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
+ // because it schedules an alarm too. Prevent it from doing so.
+ spyOn(mQuotaController);
+ doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Working set window size is 2 hours.
+ final int standbyBucket = WORKING_INDEX;
+ final long contributionMs = mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS / 2;
+ final long remainingTimeMs =
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS - contributionMs;
+
+ // Session straddles edge of 24 hour window. Only the contribution should be counted towards
+ // the quota.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - (24 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS + contributionMs, 3));
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(now - 20 * HOUR_IN_MILLIS, remainingTimeMs, 300));
+ // Expected alarm time should be when the app will have QUOTA_BUFFER_MS time of quota, which
+ // is 24 hours + (QUOTA_BUFFER_MS - contributionMs) after the start of the second session.
+ final long expectedAlarmTime = now - 20 * HOUR_IN_MILLIS
+ //+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS
+ + 24 * HOUR_IN_MILLIS
+ + (mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS - contributionMs);
+ mQuotaController.maybeScheduleStartAlarmLocked(SOURCE_USER_ID, SOURCE_PACKAGE,
+ standbyBucket);
+ verify(mAlarmManager, times(1))
+ .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+ }
+
/** Tests that QuotaController doesn't throttle if throttling is turned off. */
@Test
public void testThrottleToggling() throws Exception {
@@ -652,6 +1099,7 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 30 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 3 * HOUR_IN_MILLIS;
mQuotaController.onConstantsUpdatedLocked();
@@ -662,6 +1110,7 @@
assertEquals(45 * MINUTE_IN_MILLIS,
mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+ assertEquals(3 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
}
@Test
@@ -673,6 +1122,7 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = -MINUTE_IN_MILLIS;
mQuotaController.onConstantsUpdatedLocked();
@@ -682,6 +1132,7 @@
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+ assertEquals(HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
// Test larger than a day. Controller should cap at one day.
mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS;
@@ -690,6 +1141,7 @@
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 25 * HOUR_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS;
mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS;
+ mConstants.QUOTA_CONTROLLER_MAX_EXECUTION_TIME_MS = 25 * HOUR_IN_MILLIS;
mQuotaController.onConstantsUpdatedLocked();
@@ -699,6 +1151,7 @@
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
+ assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getMaxExecutionTimeMs());
}
/** Tests that TimingSessions aren't saved when the device is charging. */