blob: 3fc4e89e80a870ca858d3363f67c7b4b7c701048 [file] [log] [blame]
Kweku Adams4836f9d2018-11-12 17:04:17 -08001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.job.controllers;
18
19import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
20import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
21import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
22import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
23import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
24import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
25import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
26import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
27import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
28import static com.android.server.job.JobSchedulerService.RARE_INDEX;
29import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
30
31import static org.junit.Assert.assertEquals;
32import static org.junit.Assert.assertFalse;
33import static org.junit.Assert.assertNull;
34import static org.junit.Assert.assertTrue;
35import static org.mockito.ArgumentMatchers.any;
36import static org.mockito.ArgumentMatchers.anyInt;
37import static org.mockito.ArgumentMatchers.anyLong;
38import static org.mockito.Mockito.atLeast;
39import static org.mockito.Mockito.eq;
40import static org.mockito.Mockito.never;
41import static org.mockito.Mockito.timeout;
42import static org.mockito.Mockito.times;
43import static org.mockito.Mockito.verify;
44
45import android.app.AlarmManager;
46import android.app.job.JobInfo;
47import android.app.usage.UsageStatsManager;
48import android.app.usage.UsageStatsManagerInternal;
49import android.content.BroadcastReceiver;
50import android.content.ComponentName;
51import android.content.Context;
52import android.content.Intent;
53import android.content.pm.PackageManagerInternal;
54import android.os.BatteryManager;
55import android.os.BatteryManagerInternal;
56import android.os.Handler;
57import android.os.Looper;
58import android.os.SystemClock;
59
60import androidx.test.runner.AndroidJUnit4;
61
62import com.android.server.LocalServices;
63import com.android.server.job.JobSchedulerService;
64import com.android.server.job.JobSchedulerService.Constants;
65import com.android.server.job.controllers.QuotaController.TimingSession;
66
67import org.junit.After;
68import org.junit.Before;
69import org.junit.Test;
70import org.junit.runner.RunWith;
71import org.mockito.ArgumentCaptor;
72import org.mockito.InOrder;
73import org.mockito.Mock;
74import org.mockito.MockitoSession;
75import org.mockito.quality.Strictness;
76
77import java.time.Clock;
78import java.time.Duration;
79import java.time.ZoneOffset;
80import java.util.ArrayList;
81import java.util.List;
82
83@RunWith(AndroidJUnit4.class)
84public class QuotaControllerTest {
85 private static final long SECOND_IN_MILLIS = 1000L;
86 private static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
87 private static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
88 private static final String TAG_CLEANUP = "*job.cleanup*";
89 private static final String TAG_QUOTA_CHECK = "*job.quota_check*";
Kweku Adams4836f9d2018-11-12 17:04:17 -080090 private static final int CALLING_UID = 1000;
91 private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests";
92 private static final int SOURCE_USER_ID = 0;
93
94 private BroadcastReceiver mChargingReceiver;
95 private Constants mConstants;
96 private QuotaController mQuotaController;
97
98 private MockitoSession mMockingSession;
99 @Mock
100 private AlarmManager mAlarmManager;
101 @Mock
102 private Context mContext;
103 @Mock
104 private JobSchedulerService mJobSchedulerService;
105 @Mock
106 private UsageStatsManagerInternal mUsageStatsManager;
107
108 @Before
109 public void setUp() {
110 mMockingSession = mockitoSession()
111 .initMocks(this)
112 .strictness(Strictness.LENIENT)
113 .mockStatic(LocalServices.class)
114 .startMocking();
115 // Make sure constants turn on QuotaController.
116 mConstants = new Constants();
117 mConstants.USE_HEARTBEATS = false;
118
119 // Called in StateController constructor.
120 when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
121 when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
122 when(mJobSchedulerService.getConstants()).thenReturn(mConstants);
123 // Called in QuotaController constructor.
124 when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
125 when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
126 doReturn(mock(BatteryManagerInternal.class))
127 .when(() -> LocalServices.getService(BatteryManagerInternal.class));
128 doReturn(mUsageStatsManager)
129 .when(() -> LocalServices.getService(UsageStatsManagerInternal.class));
130 // Used in JobStatus.
131 doReturn(mock(PackageManagerInternal.class))
132 .when(() -> LocalServices.getService(PackageManagerInternal.class));
133
134 // Freeze the clocks at this moment in time
135 JobSchedulerService.sSystemClock =
136 Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
137 JobSchedulerService.sUptimeMillisClock =
138 Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
139 JobSchedulerService.sElapsedRealtimeClock =
140 Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
141
142 // Initialize real objects.
143 // Capture the listeners.
144 ArgumentCaptor<BroadcastReceiver> receiverCaptor =
145 ArgumentCaptor.forClass(BroadcastReceiver.class);
146 mQuotaController = new QuotaController(mJobSchedulerService);
147
148 verify(mContext).registerReceiver(receiverCaptor.capture(), any());
149 mChargingReceiver = receiverCaptor.getValue();
150 }
151
152 @After
153 public void tearDown() {
154 if (mMockingSession != null) {
155 mMockingSession.finishMocking();
156 }
157 }
158
159 private Clock getAdvancedClock(Clock clock, long incrementMs) {
160 return Clock.offset(clock, Duration.ofMillis(incrementMs));
161 }
162
163 private void advanceElapsedClock(long incrementMs) {
164 JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
165 JobSchedulerService.sElapsedRealtimeClock, incrementMs);
166 }
167
168 private void setCharging() {
169 Intent intent = new Intent(BatteryManager.ACTION_CHARGING);
170 mChargingReceiver.onReceive(mContext, intent);
171 }
172
173 private void setDischarging() {
174 Intent intent = new Intent(BatteryManager.ACTION_DISCHARGING);
175 mChargingReceiver.onReceive(mContext, intent);
176 }
177
178 private void setStandbyBucket(int bucketIndex) {
179 int bucket;
180 switch (bucketIndex) {
181 case ACTIVE_INDEX:
182 bucket = UsageStatsManager.STANDBY_BUCKET_ACTIVE;
183 break;
184 case WORKING_INDEX:
185 bucket = UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
186 break;
187 case FREQUENT_INDEX:
188 bucket = UsageStatsManager.STANDBY_BUCKET_FREQUENT;
189 break;
190 case RARE_INDEX:
191 bucket = UsageStatsManager.STANDBY_BUCKET_RARE;
192 break;
193 default:
194 bucket = UsageStatsManager.STANDBY_BUCKET_NEVER;
195 }
196 when(mUsageStatsManager.getAppStandbyBucket(eq(SOURCE_PACKAGE), eq(SOURCE_USER_ID),
197 anyLong())).thenReturn(bucket);
198 }
199
200 private void setStandbyBucket(int bucketIndex, JobStatus job) {
201 setStandbyBucket(bucketIndex);
202 job.setStandbyBucket(bucketIndex);
203 }
204
205 private JobStatus createJobStatus(String testTag, int jobId) {
206 JobInfo jobInfo = new JobInfo.Builder(jobId,
207 new ComponentName(mContext, "TestQuotaJobService"))
208 .setMinimumLatency(Math.abs(jobId) + 1)
209 .build();
210 return JobStatus.createFromJobInfo(
211 jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
212 }
213
214 private TimingSession createTimingSession(long start, long duration, int count) {
215 return new TimingSession(start, start + duration, count);
216 }
217
218 @Test
219 public void testSaveTimingSession() {
220 assertNull(mQuotaController.getTimingSessions(0, "com.android.test"));
221
222 List<TimingSession> expected = new ArrayList<>();
223 TimingSession one = new TimingSession(1, 10, 1);
224 TimingSession two = new TimingSession(11, 20, 2);
225 TimingSession thr = new TimingSession(21, 30, 3);
226
227 mQuotaController.saveTimingSession(0, "com.android.test", one);
228 expected.add(one);
229 assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
230
231 mQuotaController.saveTimingSession(0, "com.android.test", two);
232 expected.add(two);
233 assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
234
235 mQuotaController.saveTimingSession(0, "com.android.test", thr);
236 expected.add(thr);
237 assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
238 }
239
240 @Test
241 public void testDeleteObsoleteSessionsLocked() {
242 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
243 TimingSession one = createTimingSession(
244 now - 10 * MINUTE_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3);
245 TimingSession two = createTimingSession(
246 now - (70 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1);
247 TimingSession thr = createTimingSession(
248 now - (3 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1);
249 // Overlaps 24 hour boundary.
250 TimingSession fou = createTimingSession(
251 now - (24 * HOUR_IN_MILLIS + 2 * MINUTE_IN_MILLIS), 7 * MINUTE_IN_MILLIS, 1);
252 // Way past the 24 hour boundary.
253 TimingSession fiv = createTimingSession(
254 now - (25 * HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 4);
255 List<TimingSession> expected = new ArrayList<>();
256 // Added in correct (chronological) order.
257 expected.add(fou);
258 expected.add(thr);
259 expected.add(two);
260 expected.add(one);
261 mQuotaController.saveTimingSession(0, "com.android.test", fiv);
262 mQuotaController.saveTimingSession(0, "com.android.test", fou);
263 mQuotaController.saveTimingSession(0, "com.android.test", thr);
264 mQuotaController.saveTimingSession(0, "com.android.test", two);
265 mQuotaController.saveTimingSession(0, "com.android.test", one);
266
267 mQuotaController.deleteObsoleteSessionsLocked();
268
269 assertEquals(expected, mQuotaController.getTimingSessions(0, "com.android.test"));
270 }
271
272 @Test
273 public void testGetTrailingExecutionTimeLocked_NoTimer() {
274 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
275 // Added in chronological order.
276 mQuotaController.saveTimingSession(0, "com.android.test",
277 createTimingSession(now - (6 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5));
278 mQuotaController.saveTimingSession(0, "com.android.test",
279 createTimingSession(
280 now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), 6 * MINUTE_IN_MILLIS, 5));
281 mQuotaController.saveTimingSession(0, "com.android.test",
282 createTimingSession(now - (HOUR_IN_MILLIS), MINUTE_IN_MILLIS, 1));
283 mQuotaController.saveTimingSession(0, "com.android.test",
284 createTimingSession(
285 now - (HOUR_IN_MILLIS - 10 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1));
286 mQuotaController.saveTimingSession(0, "com.android.test",
287 createTimingSession(now - 5 * MINUTE_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
288
289 assertEquals(0, mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
290 MINUTE_IN_MILLIS));
291 assertEquals(2 * MINUTE_IN_MILLIS,
292 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
293 3 * MINUTE_IN_MILLIS));
294 assertEquals(4 * MINUTE_IN_MILLIS,
295 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
296 5 * MINUTE_IN_MILLIS));
297 assertEquals(4 * MINUTE_IN_MILLIS,
298 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
299 49 * MINUTE_IN_MILLIS));
300 assertEquals(5 * MINUTE_IN_MILLIS,
301 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
302 50 * MINUTE_IN_MILLIS));
303 assertEquals(6 * MINUTE_IN_MILLIS,
304 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
305 HOUR_IN_MILLIS));
306 assertEquals(11 * MINUTE_IN_MILLIS,
307 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
308 2 * HOUR_IN_MILLIS));
309 assertEquals(12 * MINUTE_IN_MILLIS,
310 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
311 3 * HOUR_IN_MILLIS));
312 assertEquals(22 * MINUTE_IN_MILLIS,
313 mQuotaController.getTrailingExecutionTimeLocked(0, "com.android.test",
314 6 * HOUR_IN_MILLIS));
315 }
316
317 @Test
318 public void testMaybeScheduleCleanupAlarmLocked() {
319 // No sessions saved yet.
320 mQuotaController.maybeScheduleCleanupAlarmLocked();
321 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_CLEANUP), any(), any());
322
323 // Test with only one timing session saved.
324 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
325 final long end = now - (6 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
326 mQuotaController.saveTimingSession(0, "com.android.test",
327 new TimingSession(now - 6 * HOUR_IN_MILLIS, end, 1));
328 mQuotaController.maybeScheduleCleanupAlarmLocked();
329 verify(mAlarmManager, times(1))
330 .set(anyInt(), eq(end + 24 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any());
331
332 // Test with new (more recent) timing sessions saved. AlarmManger shouldn't be called again.
333 mQuotaController.saveTimingSession(0, "com.android.test",
334 createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
335 mQuotaController.saveTimingSession(0, "com.android.test",
336 createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1));
337 mQuotaController.maybeScheduleCleanupAlarmLocked();
338 verify(mAlarmManager, times(1))
339 .set(anyInt(), eq(end + 24 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any());
340 }
341
342 @Test
343 public void testMaybeScheduleStartAlarmLocked_WorkingSet() {
344 // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
345 // because it schedules an alarm too. Prevent it from doing so.
346 spyOn(mQuotaController);
347 doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
348
349 // Working set window size is 2 hours.
350 final int standbyBucket = WORKING_INDEX;
351
352 // No sessions saved yet.
353 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
354 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
355
356 // Test with timing sessions out of window.
357 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
358 mQuotaController.saveTimingSession(0, "com.android.test",
359 createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
360 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
361 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
362
363 // Test with timing sessions in window but still in quota.
364 final long end = now - (2 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
365 // Counting backwards, the quota will come back one minute before the end.
366 final long expectedAlarmTime =
Kweku Adamscdbfcb92018-12-06 17:05:15 -0800367 end - MINUTE_IN_MILLIS + 2 * HOUR_IN_MILLIS
368 + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
Kweku Adams4836f9d2018-11-12 17:04:17 -0800369 mQuotaController.saveTimingSession(0, "com.android.test",
370 new TimingSession(now - 2 * HOUR_IN_MILLIS, end, 1));
371 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
372 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
373
374 // Add some more sessions, but still in quota.
375 mQuotaController.saveTimingSession(0, "com.android.test",
376 createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
377 mQuotaController.saveTimingSession(0, "com.android.test",
378 createTimingSession(now - (50 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1));
379 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
380 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
381
382 // Test when out of quota.
383 mQuotaController.saveTimingSession(0, "com.android.test",
384 createTimingSession(now - 30 * MINUTE_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
385 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
386 verify(mAlarmManager, times(1))
387 .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
388
389 // Alarm already scheduled, so make sure it's not scheduled again.
390 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
391 verify(mAlarmManager, times(1))
392 .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
393 }
394
395 @Test
396 public void testMaybeScheduleStartAlarmLocked_Frequent() {
397 // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
398 // because it schedules an alarm too. Prevent it from doing so.
399 spyOn(mQuotaController);
400 doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
401
402 // Frequent window size is 8 hours.
403 final int standbyBucket = FREQUENT_INDEX;
404
405 // No sessions saved yet.
406 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
407 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
408
409 // Test with timing sessions out of window.
410 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
411 mQuotaController.saveTimingSession(0, "com.android.test",
412 createTimingSession(now - 10 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
413 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
414 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
415
416 // Test with timing sessions in window but still in quota.
417 final long start = now - (6 * HOUR_IN_MILLIS);
Kweku Adamscdbfcb92018-12-06 17:05:15 -0800418 final long expectedAlarmTime =
419 start + 8 * HOUR_IN_MILLIS + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
Kweku Adams4836f9d2018-11-12 17:04:17 -0800420 mQuotaController.saveTimingSession(0, "com.android.test",
421 createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1));
422 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
423 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
424
425 // Add some more sessions, but still in quota.
426 mQuotaController.saveTimingSession(0, "com.android.test",
427 createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
428 mQuotaController.saveTimingSession(0, "com.android.test",
429 createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1));
430 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
431 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
432
433 // Test when out of quota.
434 mQuotaController.saveTimingSession(0, "com.android.test",
435 createTimingSession(now - HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
436 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
437 verify(mAlarmManager, times(1))
438 .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
439
440 // Alarm already scheduled, so make sure it's not scheduled again.
441 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
442 verify(mAlarmManager, times(1))
443 .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
444 }
445
446 @Test
447 public void testMaybeScheduleStartAlarmLocked_Rare() {
448 // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
449 // because it schedules an alarm too. Prevent it from doing so.
450 spyOn(mQuotaController);
451 doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
452
453 // Rare window size is 24 hours.
454 final int standbyBucket = RARE_INDEX;
455
456 // No sessions saved yet.
457 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
458 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
459
460 // Test with timing sessions out of window.
461 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
462 mQuotaController.saveTimingSession(0, "com.android.test",
463 createTimingSession(now - 25 * HOUR_IN_MILLIS, 5 * MINUTE_IN_MILLIS, 1));
464 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
465 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
466
467 // Test with timing sessions in window but still in quota.
468 final long start = now - (6 * HOUR_IN_MILLIS);
469 // Counting backwards, the first minute in the session is over the allowed time, so it
470 // needs to be excluded.
471 final long expectedAlarmTime =
Kweku Adamscdbfcb92018-12-06 17:05:15 -0800472 start + MINUTE_IN_MILLIS + 24 * HOUR_IN_MILLIS
473 + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
Kweku Adams4836f9d2018-11-12 17:04:17 -0800474 mQuotaController.saveTimingSession(0, "com.android.test",
475 createTimingSession(start, 5 * MINUTE_IN_MILLIS, 1));
476 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
477 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
478
479 // Add some more sessions, but still in quota.
480 mQuotaController.saveTimingSession(0, "com.android.test",
481 createTimingSession(now - 3 * HOUR_IN_MILLIS, MINUTE_IN_MILLIS, 1));
482 mQuotaController.saveTimingSession(0, "com.android.test",
483 createTimingSession(now - HOUR_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 1));
484 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
485 verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
486
487 // Test when out of quota.
488 mQuotaController.saveTimingSession(0, "com.android.test",
489 createTimingSession(now - HOUR_IN_MILLIS, 2 * MINUTE_IN_MILLIS, 1));
490 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
491 verify(mAlarmManager, times(1))
492 .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
493
494 // Alarm already scheduled, so make sure it's not scheduled again.
495 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
496 verify(mAlarmManager, times(1))
497 .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
498 }
499
500 /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */
501 @Test
502 public void testMaybeScheduleStartAlarmLocked_BucketChange() {
503 // saveTimingSession calls maybeScheduleCleanupAlarmLocked which interferes with these tests
504 // because it schedules an alarm too. Prevent it from doing so.
505 spyOn(mQuotaController);
506 doNothing().when(mQuotaController).maybeScheduleCleanupAlarmLocked();
507
508 final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
509
510 // Affects rare bucket
511 mQuotaController.saveTimingSession(0, "com.android.test",
512 createTimingSession(now - 12 * HOUR_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3));
513 // Affects frequent and rare buckets
514 mQuotaController.saveTimingSession(0, "com.android.test",
515 createTimingSession(now - 4 * HOUR_IN_MILLIS, 4 * MINUTE_IN_MILLIS, 3));
516 // Affects working, frequent, and rare buckets
517 final long outOfQuotaTime = now - HOUR_IN_MILLIS;
518 mQuotaController.saveTimingSession(0, "com.android.test",
519 createTimingSession(outOfQuotaTime, 7 * MINUTE_IN_MILLIS, 10));
520 // Affects all buckets
521 mQuotaController.saveTimingSession(0, "com.android.test",
522 createTimingSession(now - 5 * MINUTE_IN_MILLIS, 3 * MINUTE_IN_MILLIS, 3));
523
524 InOrder inOrder = inOrder(mAlarmManager);
525
526 // Start in ACTIVE bucket.
527 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
528 inOrder.verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
529 any());
530 inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
531
532 // And down from there.
533 final long expectedWorkingAlarmTime =
Kweku Adamscdbfcb92018-12-06 17:05:15 -0800534 outOfQuotaTime + (2 * HOUR_IN_MILLIS)
535 + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
Kweku Adams4836f9d2018-11-12 17:04:17 -0800536 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX);
537 inOrder.verify(mAlarmManager, times(1))
538 .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
539
540 final long expectedFrequentAlarmTime =
Kweku Adamscdbfcb92018-12-06 17:05:15 -0800541 outOfQuotaTime + (8 * HOUR_IN_MILLIS)
542 + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
Kweku Adams4836f9d2018-11-12 17:04:17 -0800543 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX);
544 inOrder.verify(mAlarmManager, times(1))
545 .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
546
547 final long expectedRareAlarmTime =
Kweku Adamscdbfcb92018-12-06 17:05:15 -0800548 outOfQuotaTime + (24 * HOUR_IN_MILLIS)
549 + mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS;
Kweku Adams4836f9d2018-11-12 17:04:17 -0800550 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", RARE_INDEX);
551 inOrder.verify(mAlarmManager, times(1))
552 .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
553
554 // And back up again.
555 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX);
556 inOrder.verify(mAlarmManager, times(1))
557 .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
558
559 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX);
560 inOrder.verify(mAlarmManager, times(1))
561 .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
562
563 mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
564 inOrder.verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
565 any());
566 inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
567 }
568
569 /** Tests that QuotaController doesn't throttle if throttling is turned off. */
570 @Test
571 public void testThrottleToggling() throws Exception {
572 setDischarging();
573 mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
574 createTimingSession(
575 JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
576 10 * MINUTE_IN_MILLIS, 4));
577 JobStatus jobStatus = createJobStatus("testThrottleToggling", 1);
578 setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
579 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
580 assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
581
582 mConstants.USE_HEARTBEATS = true;
583 mQuotaController.onConstantsUpdatedLocked();
584 Thread.sleep(SECOND_IN_MILLIS); // Job updates are done in the background.
585 assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
586
587 mConstants.USE_HEARTBEATS = false;
588 mQuotaController.onConstantsUpdatedLocked();
589 Thread.sleep(SECOND_IN_MILLIS); // Job updates are done in the background.
590 assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
591 }
592
593 @Test
594 public void testConstantsUpdating_ValidValues() {
595 mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 5 * MINUTE_IN_MILLIS;
596 mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS = 2 * MINUTE_IN_MILLIS;
597 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_ACTIVE_MS = 15 * MINUTE_IN_MILLIS;
598 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 30 * MINUTE_IN_MILLIS;
599 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 45 * MINUTE_IN_MILLIS;
600 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 60 * MINUTE_IN_MILLIS;
601
602 mQuotaController.onConstantsUpdatedLocked();
603
604 assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs());
605 assertEquals(2 * MINUTE_IN_MILLIS, mQuotaController.getInQuotaBufferMs());
606 assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[ACTIVE_INDEX]);
607 assertEquals(30 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
608 assertEquals(45 * MINUTE_IN_MILLIS,
609 mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
610 assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
611 }
612
613 @Test
614 public void testConstantsUpdating_InvalidValues() {
615 // Test negatives
616 mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = -MINUTE_IN_MILLIS;
617 mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS = -MINUTE_IN_MILLIS;
618 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_ACTIVE_MS = -MINUTE_IN_MILLIS;
619 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = -MINUTE_IN_MILLIS;
620 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = -MINUTE_IN_MILLIS;
621 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = -MINUTE_IN_MILLIS;
622
623 mQuotaController.onConstantsUpdatedLocked();
624
625 assertEquals(MINUTE_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs());
626 assertEquals(0, mQuotaController.getInQuotaBufferMs());
627 assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[ACTIVE_INDEX]);
628 assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
629 assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
630 assertEquals(MINUTE_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
631
632 // Test larger than a day. Controller should cap at one day.
633 mConstants.QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS = 25 * HOUR_IN_MILLIS;
634 mConstants.QUOTA_CONTROLLER_IN_QUOTA_BUFFER_MS = 25 * HOUR_IN_MILLIS;
635 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_ACTIVE_MS = 25 * HOUR_IN_MILLIS;
636 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_WORKING_MS = 25 * HOUR_IN_MILLIS;
637 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_FREQUENT_MS = 25 * HOUR_IN_MILLIS;
638 mConstants.QUOTA_CONTROLLER_WINDOW_SIZE_RARE_MS = 25 * HOUR_IN_MILLIS;
639
640 mQuotaController.onConstantsUpdatedLocked();
641
642 assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getAllowedTimePerPeriodMs());
643 assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getInQuotaBufferMs());
644 assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[ACTIVE_INDEX]);
645 assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[WORKING_INDEX]);
646 assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[FREQUENT_INDEX]);
647 assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getBucketWindowSizes()[RARE_INDEX]);
648 }
649
650 /** Tests that TimingSessions aren't saved when the device is charging. */
651 @Test
652 public void testTimerTracking_Charging() {
653 setCharging();
654
655 JobStatus jobStatus = createJobStatus("testTimerTracking_Charging", 1);
656 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
657
658 assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
659
660 mQuotaController.prepareForExecutionLocked(jobStatus);
661 advanceElapsedClock(5 * SECOND_IN_MILLIS);
662 mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
663 assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
664 }
665
666 /** Tests that TimingSessions are saved properly when the device is discharging. */
667 @Test
668 public void testTimerTracking_Discharging() {
669 setDischarging();
670
671 JobStatus jobStatus = createJobStatus("testTimerTracking_Discharging", 1);
672 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
673
674 assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
675
676 List<TimingSession> expected = new ArrayList<>();
677
678 long start = JobSchedulerService.sElapsedRealtimeClock.millis();
679 mQuotaController.prepareForExecutionLocked(jobStatus);
680 advanceElapsedClock(5 * SECOND_IN_MILLIS);
681 mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
682 expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
683 assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
684
685 // Test overlapping jobs.
686 JobStatus jobStatus2 = createJobStatus("testTimerTracking_Discharging", 2);
687 mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
688
689 JobStatus jobStatus3 = createJobStatus("testTimerTracking_Discharging", 3);
690 mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null);
691
692 advanceElapsedClock(SECOND_IN_MILLIS);
693
694 start = JobSchedulerService.sElapsedRealtimeClock.millis();
695 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
696 mQuotaController.prepareForExecutionLocked(jobStatus);
697 advanceElapsedClock(10 * SECOND_IN_MILLIS);
698 mQuotaController.prepareForExecutionLocked(jobStatus2);
699 advanceElapsedClock(10 * SECOND_IN_MILLIS);
700 mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
701 advanceElapsedClock(10 * SECOND_IN_MILLIS);
702 mQuotaController.prepareForExecutionLocked(jobStatus3);
703 advanceElapsedClock(20 * SECOND_IN_MILLIS);
704 mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
705 advanceElapsedClock(10 * SECOND_IN_MILLIS);
706 mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
707 expected.add(createTimingSession(start, MINUTE_IN_MILLIS, 3));
708 assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
709 }
710
711 /**
712 * Tests that TimingSessions are saved properly when the device alternates between
713 * charging and discharging.
714 */
715 @Test
716 public void testTimerTracking_ChargingAndDischarging() {
717 JobStatus jobStatus = createJobStatus("testTimerTracking_ChargingAndDischarging", 1);
718 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
719 JobStatus jobStatus2 = createJobStatus("testTimerTracking_ChargingAndDischarging", 2);
720 mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
721 JobStatus jobStatus3 = createJobStatus("testTimerTracking_ChargingAndDischarging", 3);
722 mQuotaController.maybeStartTrackingJobLocked(jobStatus3, null);
723 assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
724 List<TimingSession> expected = new ArrayList<>();
725
726 // A job starting while charging. Only the portion that runs during the discharging period
727 // should be counted.
728 setCharging();
729
730 mQuotaController.prepareForExecutionLocked(jobStatus);
731 advanceElapsedClock(10 * SECOND_IN_MILLIS);
732 setDischarging();
733 long start = JobSchedulerService.sElapsedRealtimeClock.millis();
734 advanceElapsedClock(10 * SECOND_IN_MILLIS);
735 mQuotaController.maybeStopTrackingJobLocked(jobStatus, jobStatus, true);
736 expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
737 assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
738
739 advanceElapsedClock(SECOND_IN_MILLIS);
740
741 // One job starts while discharging, spans a charging session, and ends after the charging
742 // session. Only the portions during the discharging periods should be counted. This should
743 // result in two TimingSessions. A second job starts while discharging and ends within the
744 // charging session. Only the portion during the first discharging portion should be
745 // counted. A third job starts and ends within the charging session. The third job
746 // shouldn't be included in either job count.
747 setDischarging();
748 start = JobSchedulerService.sElapsedRealtimeClock.millis();
749 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
750 mQuotaController.prepareForExecutionLocked(jobStatus);
751 advanceElapsedClock(10 * SECOND_IN_MILLIS);
752 mQuotaController.prepareForExecutionLocked(jobStatus2);
753 advanceElapsedClock(10 * SECOND_IN_MILLIS);
754 setCharging();
755 expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 2));
756 mQuotaController.prepareForExecutionLocked(jobStatus3);
757 advanceElapsedClock(10 * SECOND_IN_MILLIS);
758 mQuotaController.maybeStopTrackingJobLocked(jobStatus3, null, false);
759 advanceElapsedClock(10 * SECOND_IN_MILLIS);
760 mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
761 advanceElapsedClock(10 * SECOND_IN_MILLIS);
762 setDischarging();
763 start = JobSchedulerService.sElapsedRealtimeClock.millis();
764 advanceElapsedClock(20 * SECOND_IN_MILLIS);
765 mQuotaController.maybeStopTrackingJobLocked(jobStatus, null, false);
766 expected.add(createTimingSession(start, 20 * SECOND_IN_MILLIS, 1));
767 assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
768
769 // A job starting while discharging and ending while charging. Only the portion that runs
770 // during the discharging period should be counted.
771 setDischarging();
772 start = JobSchedulerService.sElapsedRealtimeClock.millis();
773 mQuotaController.maybeStartTrackingJobLocked(jobStatus2, null);
774 mQuotaController.prepareForExecutionLocked(jobStatus2);
775 advanceElapsedClock(10 * SECOND_IN_MILLIS);
776 expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
777 setCharging();
778 advanceElapsedClock(10 * SECOND_IN_MILLIS);
779 mQuotaController.maybeStopTrackingJobLocked(jobStatus2, null, false);
780 assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
781 }
782
783 /**
784 * Tests that a job is properly updated and JobSchedulerService is notified when a job reaches
785 * its quota.
786 */
787 @Test
788 public void testTracking_OutOfQuota() {
789 JobStatus jobStatus = createJobStatus("testTracking_OutOfQuota", 1);
790 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
791 setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
792 // Now the package only has two seconds to run.
793 final long remainingTimeMs = 2 * SECOND_IN_MILLIS;
794 mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
795 createTimingSession(
796 JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS,
797 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
798
799 // Start the job.
800 mQuotaController.prepareForExecutionLocked(jobStatus);
801 advanceElapsedClock(remainingTimeMs);
802
803 // Wait for some extra time to allow for job processing.
804 verify(mJobSchedulerService,
805 timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(1))
806 .onControllerStateChanged();
807 assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
808 }
809
810 /**
811 * Tests that a job is properly handled when it's at the edge of its quota and the old quota is
812 * being phased out.
813 */
814 @Test
815 public void testTracking_RollingQuota() {
816 JobStatus jobStatus = createJobStatus("testTracking_OutOfQuota", 1);
817 mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
818 setStandbyBucket(WORKING_INDEX, jobStatus); // 2 hour window
819 Handler handler = mQuotaController.getHandler();
820 spyOn(handler);
821
822 long now = JobSchedulerService.sElapsedRealtimeClock.millis();
823 final long remainingTimeMs = SECOND_IN_MILLIS;
824 // The package only has one second to run, but this session is at the edge of the rolling
825 // window, so as the package "reaches its quota" it will have more to keep running.
826 mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
827 createTimingSession(now - 2 * HOUR_IN_MILLIS,
828 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1));
829
830 assertEquals(remainingTimeMs, mQuotaController.getRemainingExecutionTimeLocked(jobStatus));
831 // Start the job.
832 mQuotaController.prepareForExecutionLocked(jobStatus);
833 advanceElapsedClock(remainingTimeMs);
834
835 // Wait for some extra time to allow for job processing.
836 verify(mJobSchedulerService,
837 timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0))
838 .onControllerStateChanged();
839 assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
840 // The job used up the remaining quota, but in that time, the same amount of time in the
841 // old TimingSession also fell out of the quota window, so it should still have the same
842 // amount of remaining time left its quota.
843 assertEquals(remainingTimeMs,
844 mQuotaController.getRemainingExecutionTimeLocked(SOURCE_USER_ID, SOURCE_PACKAGE));
845 verify(handler, atLeast(1)).sendMessageDelayed(any(), eq(remainingTimeMs));
846 }
847}