1 /*
2  * Copyright (C) 2019 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 
17 package com.android.server.job;
18 
19 import static android.text.format.DateUtils.DAY_IN_MILLIS;
20 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
21 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
22 
23 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
24 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
25 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
26 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
27 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
28 import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
29 import static com.android.server.job.JobSchedulerService.RARE_INDEX;
30 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
31 
32 import static org.junit.Assert.assertEquals;
33 import static org.junit.Assert.assertTrue;
34 import static org.junit.Assert.fail;
35 import static org.mockito.ArgumentMatchers.any;
36 import static org.mockito.ArgumentMatchers.anyInt;
37 import static org.mockito.Mockito.when;
38 
39 import android.app.ActivityManager;
40 import android.app.ActivityManagerInternal;
41 import android.app.IActivityManager;
42 import android.app.UiModeManager;
43 import android.app.job.JobInfo;
44 import android.app.job.JobScheduler;
45 import android.app.usage.UsageStatsManagerInternal;
46 import android.content.ComponentName;
47 import android.content.Context;
48 import android.content.pm.PackageManager;
49 import android.content.pm.PackageManagerInternal;
50 import android.content.res.Resources;
51 import android.net.ConnectivityManager;
52 import android.net.NetworkPolicyManager;
53 import android.os.BatteryManagerInternal;
54 import android.os.Looper;
55 import android.os.RemoteException;
56 import android.os.ServiceManager;
57 import android.os.SystemClock;
58 import android.util.Log;
59 import android.util.SparseBooleanArray;
60 import android.util.SparseLongArray;
61 
62 import com.android.server.AppStateTracker;
63 import com.android.server.AppStateTrackerImpl;
64 import com.android.server.DeviceIdleInternal;
65 import com.android.server.LocalServices;
66 import com.android.server.PowerAllowlistInternal;
67 import com.android.server.SystemServiceManager;
68 import com.android.server.job.controllers.JobStatus;
69 import com.android.server.pm.UserManagerInternal;
70 import com.android.server.usage.AppStandbyInternal;
71 
72 import org.junit.After;
73 import org.junit.Before;
74 import org.junit.Test;
75 import org.mockito.Mock;
76 import org.mockito.MockitoSession;
77 import org.mockito.quality.Strictness;
78 
79 import java.time.Clock;
80 import java.time.Duration;
81 import java.time.ZoneOffset;
82 import java.util.Random;
83 
84 public class JobSchedulerServiceTest {
85     private static final String TAG = JobSchedulerServiceTest.class.getSimpleName();
86 
87     private JobSchedulerService mService;
88 
89     private MockitoSession mMockingSession;
90     @Mock
91     private ActivityManagerInternal mActivityMangerInternal;
92     @Mock
93     private Context mContext;
94 
95     private class TestJobSchedulerService extends JobSchedulerService {
TestJobSchedulerService(Context context)96         TestJobSchedulerService(Context context) {
97             super(context);
98             mAppStateTracker = mock(AppStateTrackerImpl.class);
99         }
100 
101         @Override
isChainedAttributionEnabled()102         public boolean isChainedAttributionEnabled() {
103             return false;
104         }
105     }
106 
107     @Before
setUp()108     public void setUp() {
109         mMockingSession = mockitoSession()
110                 .initMocks(this)
111                 .strictness(Strictness.LENIENT)
112                 .mockStatic(LocalServices.class)
113                 .mockStatic(ServiceManager.class)
114                 .startMocking();
115 
116         // Called in JobSchedulerService constructor.
117         when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
118         doReturn(mActivityMangerInternal)
119                 .when(() -> LocalServices.getService(ActivityManagerInternal.class));
120         doReturn(mock(AppStandbyInternal.class))
121                 .when(() -> LocalServices.getService(AppStandbyInternal.class));
122         doReturn(mock(UsageStatsManagerInternal.class))
123                 .when(() -> LocalServices.getService(UsageStatsManagerInternal.class));
124         when(mContext.getString(anyInt())).thenReturn("some_test_string");
125         // Called in BackgroundJobsController constructor.
126         doReturn(mock(AppStateTrackerImpl.class))
127                 .when(() -> LocalServices.getService(AppStateTracker.class));
128         // Called in BatteryController constructor.
129         doReturn(mock(BatteryManagerInternal.class))
130                 .when(() -> LocalServices.getService(BatteryManagerInternal.class));
131         // Called in ConnectivityController constructor.
132         when(mContext.getSystemService(ConnectivityManager.class))
133                 .thenReturn(mock(ConnectivityManager.class));
134         when(mContext.getSystemService(NetworkPolicyManager.class))
135                 .thenReturn(mock(NetworkPolicyManager.class));
136         // Called in DeviceIdleJobsController constructor.
137         doReturn(mock(DeviceIdleInternal.class))
138                 .when(() -> LocalServices.getService(DeviceIdleInternal.class));
139         // Used in JobConcurrencyManager.
140         doReturn(mock(UserManagerInternal.class))
141                 .when(() -> LocalServices.getService(UserManagerInternal.class));
142         // Used in JobStatus.
143         doReturn(mock(PackageManagerInternal.class))
144                 .when(() -> LocalServices.getService(PackageManagerInternal.class));
145         // Called via IdleController constructor.
146         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
147         when(mContext.getResources()).thenReturn(mock(Resources.class));
148         // Called in QuotaController constructor.
149         doReturn(mock(PowerAllowlistInternal.class))
150                 .when(() -> LocalServices.getService(PowerAllowlistInternal.class));
151         IActivityManager activityManager = ActivityManager.getService();
152         spyOn(activityManager);
153         try {
154             doNothing().when(activityManager).registerUidObserver(any(), anyInt(), anyInt(), any());
155         } catch (RemoteException e) {
156             fail("registerUidObserver threw exception: " + e.getMessage());
157         }
158         // Called by QuotaTracker
159         doReturn(mock(SystemServiceManager.class))
160                 .when(() -> LocalServices.getService(SystemServiceManager.class));
161 
162         JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
163         JobSchedulerService.sElapsedRealtimeClock =
164                 Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
165         // Called by DeviceIdlenessTracker
166         when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class));
167 
168         mService = new TestJobSchedulerService(mContext);
169     }
170 
171     @After
tearDown()172     public void tearDown() {
173         if (mMockingSession != null) {
174             mMockingSession.finishMocking();
175         }
176     }
177 
getAdvancedClock(Clock clock, long incrementMs)178     private Clock getAdvancedClock(Clock clock, long incrementMs) {
179         return Clock.offset(clock, Duration.ofMillis(incrementMs));
180     }
181 
advanceElapsedClock(long incrementMs)182     private void advanceElapsedClock(long incrementMs) {
183         JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(
184                 JobSchedulerService.sElapsedRealtimeClock, incrementMs);
185     }
186 
createJobInfo()187     private static JobInfo.Builder createJobInfo() {
188         return createJobInfo(351);
189     }
190 
createJobInfo(int jobId)191     private static JobInfo.Builder createJobInfo(int jobId) {
192         return new JobInfo.Builder(jobId, new ComponentName("foo", "bar"));
193     }
194 
createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder)195     private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder) {
196         return createJobStatus(testTag, jobInfoBuilder, 1234);
197     }
198 
createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder, int callingUid)199     private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
200             int callingUid) {
201         return JobStatus.createFromJobInfo(
202                 jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
203     }
204 
205     /**
206      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
207      * with the correct delay and deadline constraints if the periodic job is scheduled with the
208      * minimum possible period.
209      */
210     @Test
testGetRescheduleJobForPeriodic_minPeriod()211     public void testGetRescheduleJobForPeriodic_minPeriod() {
212         final long now = sElapsedRealtimeClock.millis();
213         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
214                 createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS));
215         final long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS;
216         final long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS;
217 
218         for (int i = 0; i < 25; i++) {
219             JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
220             assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
221             assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
222             advanceElapsedClock(30_000); // 30 seconds
223         }
224 
225         for (int i = 0; i < 5; i++) {
226             // Window buffering in last 1/6 of window.
227             JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
228             assertEquals(nextWindowStartTime + i * 30_000, rescheduledJob.getEarliestRunTime());
229             assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
230             advanceElapsedClock(30_000); // 30 seconds
231         }
232     }
233 
234     /**
235      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
236      * with the correct delay and deadline constraints if the periodic job is scheduled with a
237      * period that's too large.
238      */
239     @Test
testGetRescheduleJobForPeriodic_largePeriod()240     public void testGetRescheduleJobForPeriodic_largePeriod() {
241         final long now = sElapsedRealtimeClock.millis();
242         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
243                 createJobInfo().setPeriodic(2 * 365 * DAY_IN_MILLIS));
244         assertEquals(now, job.getEarliestRunTime());
245         // Periods are capped at 365 days (1 year).
246         assertEquals(now + 365 * DAY_IN_MILLIS, job.getLatestRunTimeElapsed());
247         final long nextWindowStartTime = now + 365 * DAY_IN_MILLIS;
248         final long nextWindowEndTime = nextWindowStartTime + 365 * DAY_IN_MILLIS;
249 
250         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
251         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
252         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
253     }
254 
255     /**
256      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
257      * with the correct delay and deadline constraints if the periodic job is completed and
258      * rescheduled while run in its expected running window.
259      */
260     @Test
testGetRescheduleJobForPeriodic_insideWindow()261     public void testGetRescheduleJobForPeriodic_insideWindow() {
262         final long now = sElapsedRealtimeClock.millis();
263         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow",
264                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
265         final long nextWindowStartTime = now + HOUR_IN_MILLIS;
266         final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
267 
268         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
269         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
270         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
271 
272         advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes
273 
274         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
275         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
276         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
277 
278         advanceElapsedClock(20 * MINUTE_IN_MILLIS); // now + 30 minutes
279 
280         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
281         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
282         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
283 
284         advanceElapsedClock(25 * MINUTE_IN_MILLIS); // now + 55 minutes
285 
286         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
287         // Shifted because it's close to the end of the window.
288         assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS,
289                 rescheduledJob.getEarliestRunTime());
290         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
291 
292         advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 59 minutes
293 
294         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
295         // Shifted because it's close to the end of the window.
296         assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS,
297                 rescheduledJob.getEarliestRunTime());
298         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
299     }
300 
301     /**
302      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
303      * with an extra delay and correct deadline constraint if the periodic job is completed near the
304      * end of its expected running window.
305      */
306     @Test
testGetRescheduleJobForPeriodic_closeToEndOfWindow()307     public void testGetRescheduleJobForPeriodic_closeToEndOfWindow() {
308         JobStatus frequentJob = createJobStatus(
309                 "testGetRescheduleJobForPeriodic_closeToEndOfWindow",
310                 createJobInfo().setPeriodic(15 * MINUTE_IN_MILLIS));
311         long now = sElapsedRealtimeClock.millis();
312         long nextWindowStartTime = now + 15 * MINUTE_IN_MILLIS;
313         long nextWindowEndTime = now + 30 * MINUTE_IN_MILLIS;
314 
315         // At the beginning of the window. Next window should be unaffected.
316         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
317         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
318         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
319 
320         // Halfway through window. Next window should be unaffected.
321         advanceElapsedClock((long) (7.5 * MINUTE_IN_MILLIS));
322         rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
323         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
324         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
325 
326         // In last 1/6 of window. Next window start time should be shifted slightly.
327         advanceElapsedClock(6 * MINUTE_IN_MILLIS);
328         rescheduledJob = mService.getRescheduleJobForPeriodic(frequentJob);
329         assertEquals(nextWindowStartTime + MINUTE_IN_MILLIS,
330                 rescheduledJob.getEarliestRunTime());
331         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
332 
333         JobStatus mediumJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
334                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
335         now = sElapsedRealtimeClock.millis();
336         nextWindowStartTime = now + HOUR_IN_MILLIS;
337         nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
338 
339         // At the beginning of the window. Next window should be unaffected.
340         rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
341         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
342         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
343 
344         // Halfway through window. Next window should be unaffected.
345         advanceElapsedClock(30 * MINUTE_IN_MILLIS);
346         rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
347         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
348         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
349 
350         // At the edge 1/6 of window. Next window should be unaffected.
351         advanceElapsedClock(20 * MINUTE_IN_MILLIS);
352         rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
353         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
354         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
355 
356         // In last 1/6 of window. Next window start time should be shifted slightly.
357         advanceElapsedClock(6 * MINUTE_IN_MILLIS);
358         rescheduledJob = mService.getRescheduleJobForPeriodic(mediumJob);
359         assertEquals(nextWindowStartTime + (6 * MINUTE_IN_MILLIS),
360                 rescheduledJob.getEarliestRunTime());
361         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
362 
363         JobStatus longJob = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
364                 createJobInfo().setPeriodic(6 * HOUR_IN_MILLIS));
365         now = sElapsedRealtimeClock.millis();
366         nextWindowStartTime = now + 6 * HOUR_IN_MILLIS;
367         nextWindowEndTime = now + 12 * HOUR_IN_MILLIS;
368 
369         // At the beginning of the window. Next window should be unaffected.
370         rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
371         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
372         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
373 
374         // Halfway through window. Next window should be unaffected.
375         advanceElapsedClock(3 * HOUR_IN_MILLIS);
376         rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
377         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
378         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
379 
380         // At the edge 1/6 of window. Next window should be unaffected.
381         advanceElapsedClock(2 * HOUR_IN_MILLIS);
382         rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
383         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
384         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
385 
386         // In last 1/6 of window. Next window should be unaffected since we're over the shift cap.
387         advanceElapsedClock(15 * MINUTE_IN_MILLIS);
388         rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
389         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
390         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
391 
392         // In last 1/6 of window. Next window start time should be shifted slightly.
393         advanceElapsedClock(30 * MINUTE_IN_MILLIS);
394         rescheduledJob = mService.getRescheduleJobForPeriodic(longJob);
395         assertEquals(nextWindowStartTime + (30 * MINUTE_IN_MILLIS),
396                 rescheduledJob.getEarliestRunTime());
397         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
398 
399         // Flex duration close to period duration.
400         JobStatus gameyFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
401                 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 59 * MINUTE_IN_MILLIS));
402         now = sElapsedRealtimeClock.millis();
403         nextWindowStartTime = now + HOUR_IN_MILLIS + MINUTE_IN_MILLIS;
404         nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
405         advanceElapsedClock(MINUTE_IN_MILLIS);
406 
407         // At the beginning of the window. Next window should be unaffected.
408         rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
409         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
410         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
411 
412         // Halfway through window. Next window should be unaffected.
413         advanceElapsedClock(29 * MINUTE_IN_MILLIS);
414         rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
415         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
416         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
417 
418         // At the edge 1/6 of window. Next window should be unaffected.
419         advanceElapsedClock(20 * MINUTE_IN_MILLIS);
420         rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
421         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
422         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
423 
424         // In last 1/6 of window. Next window start time should be shifted slightly.
425         advanceElapsedClock(6 * MINUTE_IN_MILLIS);
426         rescheduledJob = mService.getRescheduleJobForPeriodic(gameyFlex);
427         assertEquals(nextWindowStartTime + (5 * MINUTE_IN_MILLIS),
428                 rescheduledJob.getEarliestRunTime());
429         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
430 
431         // Very short flex duration compared to period duration.
432         JobStatus superFlex = createJobStatus("testGetRescheduleJobForPeriodic_closeToEndOfWindow",
433                 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 10 * MINUTE_IN_MILLIS));
434         now = sElapsedRealtimeClock.millis();
435         nextWindowStartTime = now + HOUR_IN_MILLIS + 50 * MINUTE_IN_MILLIS;
436         nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
437         advanceElapsedClock(MINUTE_IN_MILLIS);
438 
439         // At the beginning of the window. Next window should be unaffected.
440         rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
441         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
442         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
443 
444         // Halfway through window. Next window should be unaffected.
445         advanceElapsedClock(29 * MINUTE_IN_MILLIS);
446         rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
447         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
448         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
449 
450         // At the edge 1/6 of window. Next window should be unaffected.
451         advanceElapsedClock(20 * MINUTE_IN_MILLIS);
452         rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
453         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
454         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
455 
456         // In last 1/6 of window. Next window should be unaffected since the flex duration pushes
457         // the next window start time far enough away.
458         advanceElapsedClock(6 * MINUTE_IN_MILLIS);
459         rescheduledJob = mService.getRescheduleJobForPeriodic(superFlex);
460         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
461         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
462     }
463 
464     /**
465      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
466      * with the correct delay and deadline constraints if the periodic job with a custom flex
467      * setting is completed and rescheduled while run in its expected running window.
468      */
469     @Test
testGetRescheduleJobForPeriodic_insideWindow_flex()470     public void testGetRescheduleJobForPeriodic_insideWindow_flex() {
471         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_flex",
472                 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
473         // First window starts 30 minutes from now.
474         advanceElapsedClock(30 * MINUTE_IN_MILLIS);
475         final long now = sElapsedRealtimeClock.millis();
476         final long nextWindowStartTime = now + HOUR_IN_MILLIS;
477         final long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
478 
479         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
480         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
481         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
482 
483         advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 10 minutes
484 
485         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
486         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
487         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
488 
489         advanceElapsedClock(15 * MINUTE_IN_MILLIS); // now + 25 minutes
490 
491         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
492         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
493         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
494 
495         advanceElapsedClock(4 * MINUTE_IN_MILLIS); // now + 29 minutes
496 
497         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
498         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
499         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
500     }
501 
502     /**
503      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
504      * with the correct delay and deadline constraints if the periodic job failed but then ran
505      * successfully and was rescheduled while run in its expected running window.
506      */
507     @Test
testGetRescheduleJobForPeriodic_insideWindow_failedJob()508     public void testGetRescheduleJobForPeriodic_insideWindow_failedJob() {
509         final long now = sElapsedRealtimeClock.millis();
510         final long nextWindowStartTime = now + HOUR_IN_MILLIS;
511         final long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
512         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob",
513                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
514         JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
515 
516         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
517         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
518         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
519 
520         advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes
521         failedJob = mService.getRescheduleJobForFailureLocked(job);
522         advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes
523 
524         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
525         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
526         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
527 
528         advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes
529         failedJob = mService.getRescheduleJobForFailureLocked(job);
530         advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes
531 
532         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
533         // Shifted because it's close to the end of the window.
534         assertEquals(nextWindowStartTime + 5 * MINUTE_IN_MILLIS,
535                 rescheduledJob.getEarliestRunTime());
536         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
537 
538         advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes
539         failedJob = mService.getRescheduleJobForFailureLocked(job);
540         advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes
541 
542         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
543         // Shifted because it's close to the end of the window.
544         assertEquals(nextWindowStartTime + 9 * MINUTE_IN_MILLIS,
545                 rescheduledJob.getEarliestRunTime());
546         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
547     }
548 
549     /**
550      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
551      * with the correct delay and deadline constraints if the periodic job is completed and
552      * rescheduled when run after its expected running window.
553      */
554     @Test
testGetRescheduleJobForPeriodic_outsideWindow()555     public void testGetRescheduleJobForPeriodic_outsideWindow() {
556         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow",
557                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
558         long now = sElapsedRealtimeClock.millis();
559         long nextWindowStartTime = now + HOUR_IN_MILLIS;
560         long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
561 
562         advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
563         // Say the job ran at the very end of its previous window. The intended JSS behavior is to
564         // have consistent windows, so the new window should start as soon as the previous window
565         // ended and end PERIOD time after the previous window ended.
566         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
567         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
568         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
569 
570         advanceElapsedClock(2 * HOUR_IN_MILLIS);
571         // Say that the job ran at this point, possibly due to device idle.
572         // The next window should be consistent (start and end at the time it would have had the job
573         // run normally in previous windows).
574         nextWindowStartTime += 2 * HOUR_IN_MILLIS;
575         nextWindowEndTime += 2 * HOUR_IN_MILLIS;
576 
577         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
578         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
579         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
580     }
581 
582     /**
583      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
584      * with the correct delay and deadline constraints if the periodic job with a custom flex
585      * setting is completed and rescheduled when run after its expected running window.
586      */
587     @Test
testGetRescheduleJobForPeriodic_outsideWindow_flex()588     public void testGetRescheduleJobForPeriodic_outsideWindow_flex() {
589         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_flex",
590                 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
591         // First window starts 30 minutes from now.
592         advanceElapsedClock(30 * MINUTE_IN_MILLIS);
593         long now = sElapsedRealtimeClock.millis();
594         long nextWindowStartTime = now + HOUR_IN_MILLIS;
595         long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
596 
597         advanceElapsedClock(31 * MINUTE_IN_MILLIS);
598         // Say the job ran at the very end of its previous window. The intended JSS behavior is to
599         // have consistent windows, so the new window should start as soon as the previous window
600         // ended and end PERIOD time after the previous window ended.
601         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(job);
602         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
603         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
604 
605         // 5 minutes before the start of the next window. It's too close to the next window, so the
606         // returned job should be for the window after.
607         advanceElapsedClock(24 * MINUTE_IN_MILLIS);
608         nextWindowStartTime += HOUR_IN_MILLIS;
609         nextWindowEndTime += HOUR_IN_MILLIS;
610         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
611         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
612         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
613 
614         advanceElapsedClock(2 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS);
615         // Say that the job ran at this point, possibly due to device idle.
616         // The next window should be consistent (start and end at the time it would have had the job
617         // run normally in previous windows).
618         nextWindowStartTime += 2 * HOUR_IN_MILLIS;
619         nextWindowEndTime += 2 * HOUR_IN_MILLIS;
620 
621         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
622         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
623         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
624     }
625 
626     /**
627      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
628      * with the correct delay and deadline constraints if the periodic job failed but then ran
629      * successfully and was rescheduled when run after its expected running window.
630      */
631     @Test
testGetRescheduleJobForPeriodic_outsideWindow_failedJob()632     public void testGetRescheduleJobForPeriodic_outsideWindow_failedJob() {
633         JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob",
634                 createJobInfo().setPeriodic(HOUR_IN_MILLIS));
635         JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
636         long now = sElapsedRealtimeClock.millis();
637         long nextWindowStartTime = now + HOUR_IN_MILLIS;
638         long nextWindowEndTime = now + 2 * HOUR_IN_MILLIS;
639 
640         advanceElapsedClock(HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
641         // Say the job ran at the very end of its previous window. The intended JSS behavior is to
642         // have consistent windows, so the new window should start as soon as the previous window
643         // ended and end PERIOD time after the previous window ended.
644         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
645         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
646         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
647 
648         advanceElapsedClock(2 * HOUR_IN_MILLIS);
649         // Say that the job ran at this point, possibly due to device idle.
650         // The next window should be consistent (start and end at the time it would have had the job
651         // run normally in previous windows).
652         nextWindowStartTime += 2 * HOUR_IN_MILLIS;
653         nextWindowEndTime += 2 * HOUR_IN_MILLIS;
654 
655         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
656         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
657         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
658     }
659 
660     /**
661      * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
662      * with the correct delay and deadline constraints if the periodic job with a custom flex
663      * setting failed but then ran successfully and was rescheduled when run after its expected
664      * running window.
665      */
666     @Test
testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob()667     public void testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob() {
668         JobStatus job = createJobStatus(
669                 "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob",
670                 createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS));
671         JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job);
672         // First window starts 30 minutes from now.
673         advanceElapsedClock(30 * MINUTE_IN_MILLIS);
674         long now = sElapsedRealtimeClock.millis();
675         long nextWindowStartTime = now + HOUR_IN_MILLIS;
676         long nextWindowEndTime = now + 90 * MINUTE_IN_MILLIS;
677 
678         advanceElapsedClock(31 * MINUTE_IN_MILLIS);
679         // Say the job ran at the very end of its previous window. The intended JSS behavior is to
680         // have consistent windows, so the new window should start as soon as the previous window
681         // ended and end PERIOD time after the previous window ended.
682         JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
683         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
684         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
685 
686         // 5 minutes before the start of the next window. It's too close to the next window, so the
687         // returned job should be for the window after.
688         advanceElapsedClock(24 * MINUTE_IN_MILLIS);
689         nextWindowStartTime += HOUR_IN_MILLIS;
690         nextWindowEndTime += HOUR_IN_MILLIS;
691         rescheduledJob = mService.getRescheduleJobForPeriodic(job);
692         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
693         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
694 
695         advanceElapsedClock(2 * HOUR_IN_MILLIS);
696         // Say that the job ran at this point, possibly due to device idle.
697         // The next window should be consistent (start and end at the time it would have had the job
698         // run normally in previous windows).
699         nextWindowStartTime += 2 * HOUR_IN_MILLIS;
700         nextWindowEndTime += 2 * HOUR_IN_MILLIS;
701 
702         rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob);
703         assertEquals(nextWindowStartTime, rescheduledJob.getEarliestRunTime());
704         assertEquals(nextWindowEndTime, rescheduledJob.getLatestRunTimeElapsed());
705     }
706 
707     /** Tests that rare job batching works as expected. */
708     @Test
testRareJobBatching()709     public void testRareJobBatching() {
710         spyOn(mService);
711         doNothing().when(mService).evaluateControllerStatesLocked(any());
712         doNothing().when(mService).noteJobsPending(any());
713         doReturn(true).when(mService).isReadyToBeExecutedLocked(any());
714         advanceElapsedClock(24 * HOUR_IN_MILLIS);
715 
716         JobSchedulerService.MaybeReadyJobQueueFunctor maybeQueueFunctor =
717                 mService.new MaybeReadyJobQueueFunctor();
718         mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
719         mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = HOUR_IN_MILLIS;
720 
721         JobStatus job = createJobStatus(
722                 "testRareJobBatching",
723                 createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
724         job.setStandbyBucket(RARE_INDEX);
725 
726         // Not enough RARE jobs to run.
727         mService.mPendingJobs.clear();
728         maybeQueueFunctor.reset();
729         for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
730             maybeQueueFunctor.accept(job);
731             assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
732             assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
733             assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
734         }
735         maybeQueueFunctor.postProcessLocked();
736         assertEquals(0, mService.mPendingJobs.size());
737 
738         // Enough RARE jobs to run.
739         mService.mPendingJobs.clear();
740         maybeQueueFunctor.reset();
741         for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT; ++i) {
742             maybeQueueFunctor.accept(job);
743             assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
744             assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
745             assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
746         }
747         maybeQueueFunctor.postProcessLocked();
748         assertEquals(5, mService.mPendingJobs.size());
749 
750         // Not enough RARE jobs to run, but a non-batched job saves the day.
751         mService.mPendingJobs.clear();
752         maybeQueueFunctor.reset();
753         JobStatus activeJob = createJobStatus(
754                 "testRareJobBatching",
755                 createJobInfo().setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
756         activeJob.setStandbyBucket(ACTIVE_INDEX);
757         for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
758             maybeQueueFunctor.accept(job);
759             assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
760             assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
761             assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
762         }
763         maybeQueueFunctor.accept(activeJob);
764         maybeQueueFunctor.postProcessLocked();
765         assertEquals(3, mService.mPendingJobs.size());
766 
767         // Not enough RARE jobs to run, but an old RARE job saves the day.
768         mService.mPendingJobs.clear();
769         maybeQueueFunctor.reset();
770         JobStatus oldRareJob = createJobStatus("testRareJobBatching", createJobInfo());
771         oldRareJob.setStandbyBucket(RARE_INDEX);
772         final long oldBatchTime = sElapsedRealtimeClock.millis()
773                 - 2 * mService.mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
774         oldRareJob.setFirstForceBatchedTimeElapsed(oldBatchTime);
775         for (int i = 0; i < mService.mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT / 2; ++i) {
776             maybeQueueFunctor.accept(job);
777             assertEquals(i + 1, maybeQueueFunctor.forceBatchedCount);
778             assertEquals(i + 1, maybeQueueFunctor.runnableJobs.size());
779             assertEquals(sElapsedRealtimeClock.millis(), job.getFirstForceBatchedTimeElapsed());
780         }
781         maybeQueueFunctor.accept(oldRareJob);
782         assertEquals(oldBatchTime, oldRareJob.getFirstForceBatchedTimeElapsed());
783         maybeQueueFunctor.postProcessLocked();
784         assertEquals(3, mService.mPendingJobs.size());
785     }
786 
787     /** Tests that jobs scheduled by the app itself are counted towards scheduling limits. */
788     @Test
testScheduleLimiting_RegularSchedule_Blocked()789     public void testScheduleLimiting_RegularSchedule_Blocked() {
790         mService.mConstants.ENABLE_API_QUOTAS = true;
791         mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
792         mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
793         mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
794         mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true;
795         mService.updateQuotaTracker();
796 
797         final JobInfo job = createJobInfo().setPersisted(true).build();
798         for (int i = 0; i < 500; ++i) {
799             final int expected =
800                     i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
801             assertEquals("Got unexpected result for schedule #" + (i + 1),
802                     expected,
803                     mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
804         }
805     }
806 
807     /**
808      * Tests that jobs scheduled by the app itself succeed even if the app is above the scheduling
809      * limit.
810      */
811     @Test
812     public void testScheduleLimiting_RegularSchedule_Allowed() {
813         mService.mConstants.ENABLE_API_QUOTAS = true;
814         mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
815         mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
816         mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
817         mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;
818         mService.updateQuotaTracker();
819 
820         final JobInfo job = createJobInfo().setPersisted(true).build();
821         for (int i = 0; i < 500; ++i) {
822             assertEquals("Got unexpected result for schedule #" + (i + 1),
823                     JobScheduler.RESULT_SUCCESS,
824                     mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
825         }
826     }
827 
828     /**
829      * Tests that jobs scheduled through a proxy (eg. system server) don't count towards scheduling
830      * limits.
831      */
832     @Test
833     public void testScheduleLimiting_Proxy() {
834         mService.mConstants.ENABLE_API_QUOTAS = true;
835         mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
836         mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
837         mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
838         mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true;
839         mService.updateQuotaTracker();
840 
841         final JobInfo job = createJobInfo().setPersisted(true).build();
842         for (int i = 0; i < 500; ++i) {
843             assertEquals("Got unexpected result for schedule #" + (i + 1),
844                     JobScheduler.RESULT_SUCCESS,
845                     mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, ""));
846         }
847     }
848 
849     /**
850      * Tests that jobs scheduled by an app for itself as if through a proxy are counted towards
851      * scheduling limits.
852      */
853     @Test
854     public void testScheduleLimiting_SelfProxy() {
855         mService.mConstants.ENABLE_API_QUOTAS = true;
856         mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300;
857         mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000;
858         mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false;
859         mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true;
860         mService.updateQuotaTracker();
861 
862         final JobInfo job = createJobInfo().setPersisted(true).build();
863         for (int i = 0; i < 500; ++i) {
864             final int expected =
865                     i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
866             assertEquals("Got unexpected result for schedule #" + (i + 1),
867                     expected,
868                     mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(),
869                             0, ""));
870         }
871     }
872 
873     @Test
874     public void testPendingJobSorting() {
875         // First letter in job variable name indicate regular (r) or expedited (e).
876         // Capital letters in job variable name indicate the app/UID.
877         // Numbers in job variable name indicate the enqueue time.
878         // Expected sort order:
879         //   eA7 > rA1 > eB6 > rB2 > eC3 > rD4 > eE5 > eF9 > rF8 > eC11 > rC10 > rG12 > rG13 > eE14
880         // Intentions:
881         //   * A jobs let us test skipping both regular and expedited jobs of other apps
882         //   * B jobs let us test skipping only regular job of another app without going too far
883         //   * C jobs test that regular jobs don't skip over other app's jobs and that EJs only
884         //     skip up to level of the earliest regular job
885         //   * E jobs test that expedited jobs don't skip the line when the app has no regular jobs
886         //   * F jobs test correct expedited/regular ordering doesn't push jobs too high in list
887         //   * G jobs test correct ordering for regular jobs
888         //   * H job tests correct behavior when enqueue times are the same
889         JobStatus rA1 = createJobStatus("testPendingJobSorting", createJobInfo(1), 1);
890         JobStatus rB2 = createJobStatus("testPendingJobSorting", createJobInfo(2), 2);
891         JobStatus eC3 = createJobStatus("testPendingJobSorting",
892                 createJobInfo(3).setExpedited(true), 3);
893         JobStatus rD4 = createJobStatus("testPendingJobSorting", createJobInfo(4), 4);
894         JobStatus eE5 = createJobStatus("testPendingJobSorting",
895                 createJobInfo(5).setExpedited(true), 5);
896         JobStatus eB6 = createJobStatus("testPendingJobSorting",
897                 createJobInfo(6).setExpedited(true), 2);
898         JobStatus eA7 = createJobStatus("testPendingJobSorting",
899                 createJobInfo(7).setExpedited(true), 1);
900         JobStatus rH8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 14);
901         JobStatus rF8 = createJobStatus("testPendingJobSorting", createJobInfo(8), 6);
902         JobStatus eF9 = createJobStatus("testPendingJobSorting",
903                 createJobInfo(9).setExpedited(true), 6);
904         JobStatus rC10 = createJobStatus("testPendingJobSorting", createJobInfo(10), 3);
905         JobStatus eC11 = createJobStatus("testPendingJobSorting",
906                 createJobInfo(11).setExpedited(true), 3);
907         JobStatus rG12 = createJobStatus("testPendingJobSorting", createJobInfo(12), 7);
908         JobStatus rG13 = createJobStatus("testPendingJobSorting", createJobInfo(13), 7);
909         JobStatus eE14 = createJobStatus("testPendingJobSorting",
910                 createJobInfo(14).setExpedited(true), 5);
911 
912         rA1.enqueueTime = 1;
913         rB2.enqueueTime = 2;
914         eC3.enqueueTime = 3;
915         rD4.enqueueTime = 4;
916         eE5.enqueueTime = 5;
917         eB6.enqueueTime = 6;
918         eA7.enqueueTime = 7;
919         rF8.enqueueTime = 8;
920         rH8.enqueueTime = 8;
921         eF9.enqueueTime = 9;
922         rC10.enqueueTime = 10;
923         eC11.enqueueTime = 11;
924         rG12.enqueueTime = 12;
925         rG13.enqueueTime = 13;
926         eE14.enqueueTime = 14;
927 
928         mService.mPendingJobs.clear();
929         // Add in random order so sorting is apparent.
930         mService.mPendingJobs.add(eC3);
931         mService.mPendingJobs.add(eE5);
932         mService.mPendingJobs.add(rA1);
933         mService.mPendingJobs.add(rG13);
934         mService.mPendingJobs.add(rD4);
935         mService.mPendingJobs.add(eA7);
936         mService.mPendingJobs.add(rG12);
937         mService.mPendingJobs.add(rH8);
938         mService.mPendingJobs.add(rF8);
939         mService.mPendingJobs.add(eB6);
940         mService.mPendingJobs.add(eE14);
941         mService.mPendingJobs.add(eF9);
942         mService.mPendingJobs.add(rB2);
943         mService.mPendingJobs.add(rC10);
944         mService.mPendingJobs.add(eC11);
945 
946         mService.mPendingJobComparator.refreshLocked();
947         mService.mPendingJobs.sort(mService.mPendingJobComparator);
948 
949         final JobStatus[] expectedOrder = new JobStatus[]{
950                 eA7, rA1, eB6, rB2, eC3, rD4, eE5, eF9, rH8, rF8, eC11, rC10, rG12, rG13, eE14};
951         for (int i = 0; i < expectedOrder.length; ++i) {
952             assertEquals("List wasn't correctly sorted @ index " + i,
953                     expectedOrder[i].getJobId(), mService.mPendingJobs.get(i).getJobId());
954         }
955     }
956 
957     private void checkPendingJobInvariants() {
958         long regJobEnqueueTime = 0;
959         final SparseBooleanArray regJobSeen = new SparseBooleanArray();
960         final SparseLongArray ejEnqueueTimes = new SparseLongArray();
961 
962         for (int i = 0; i < mService.mPendingJobs.size(); ++i) {
963             final JobStatus job = mService.mPendingJobs.get(i);
964             final int uid = job.getSourceUid();
965 
966             if (!job.isRequestedExpeditedJob()) {
967                 // Invariant #1: Regular jobs are sorted by enqueue time.
968                 assertTrue("Regular job with earlier enqueue time sorted after a later time: "
969                                 + regJobEnqueueTime + " vs " + job.enqueueTime,
970                         regJobEnqueueTime <= job.enqueueTime);
971                 regJobEnqueueTime = job.enqueueTime;
972                 regJobSeen.put(uid, true);
973             } else {
974                 // Invariant #2: EJs should be before regular jobs for an individual app
975                 if (regJobSeen.get(uid)) {
976                     fail("UID " + uid + " had an EJ ordered after a regular job");
977                 }
978                 final long ejEnqueueTime = ejEnqueueTimes.get(uid, 0);
979                 // Invariant #3: EJs for an individual app should be sorted by enqueue time.
980                 assertTrue("EJ with earlier enqueue time sorted after a later time: "
981                                 + ejEnqueueTime + " vs " + job.enqueueTime,
982                         ejEnqueueTime <= job.enqueueTime);
983                 ejEnqueueTimes.put(uid, job.enqueueTime);
984             }
985         }
986     }
987 
988     private static String sortedJobToString(JobStatus job) {
989         return "testJob " + job.getSourceUid() + "/" + job.getJobId() + "/"
990                 + job.isRequestedExpeditedJob() + "@" + job.enqueueTime;
991     }
992 
993     @Test
994     public void testPendingJobSorting_Random() {
995         Random random = new Random(1); // Always use the same series of pseudo random values.
996 
997         mService.mPendingJobs.clear();
998 
999         for (int i = 0; i < 2500; ++i) {
1000             JobStatus job = createJobStatus("testPendingJobSorting_Random",
1001                     createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250));
1002             job.enqueueTime = random.nextInt(1_000_000);
1003             mService.mPendingJobs.add(job);
1004 
1005             mService.mPendingJobComparator.refreshLocked();
1006             try {
1007                 mService.mPendingJobs.sort(mService.mPendingJobComparator);
1008             } catch (Exception e) {
1009                 for (JobStatus toDump : mService.mPendingJobs) {
1010                     Log.i(TAG, sortedJobToString(toDump));
1011                 }
1012                 throw e;
1013             }
1014             checkPendingJobInvariants();
1015         }
1016     }
1017 
1018     private int sign(int i) {
1019         if (i > 0) {
1020             return 1;
1021         }
1022         if (i < 0) {
1023             return -1;
1024         }
1025         return 0;
1026     }
1027 
1028     @Test
1029     public void testPendingJobSortingTransitivity() {
1030         // Always use the same series of pseudo random values.
1031         for (int seed : new int[]{1337, 7357, 606, 6357, 41106010, 3, 2, 1}) {
1032             Random random = new Random(seed);
1033 
1034             mService.mPendingJobs.clear();
1035 
1036             for (int i = 0; i < 300; ++i) {
1037                 JobStatus job = createJobStatus("testPendingJobSortingTransitivity",
1038                         createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(50));
1039                 job.enqueueTime = random.nextInt(1_000_000);
1040                 job.overrideState = random.nextInt(4);
1041                 mService.mPendingJobs.add(job);
1042             }
1043 
1044             verifyPendingJobComparatorTransitivity();
1045         }
1046     }
1047 
1048     @Test
1049     public void testPendingJobSortingTransitivity_Concentrated() {
1050         // Always use the same series of pseudo random values.
1051         for (int seed : new int[]{1337, 6000, 637739, 6357, 1, 7, 13}) {
1052             Random random = new Random(seed);
1053 
1054             mService.mPendingJobs.clear();
1055 
1056             for (int i = 0; i < 300; ++i) {
1057                 JobStatus job = createJobStatus("testPendingJobSortingTransitivity_Concentrated",
1058                         createJobInfo(i).setExpedited(random.nextFloat() < .03),
1059                         random.nextInt(20));
1060                 job.enqueueTime = random.nextInt(250);
1061                 job.overrideState = random.nextFloat() < .01
1062                         ? JobStatus.OVERRIDE_SORTING : JobStatus.OVERRIDE_NONE;
1063                 mService.mPendingJobs.add(job);
1064                 Log.d(TAG, sortedJobToString(job));
1065             }
1066 
1067             verifyPendingJobComparatorTransitivity();
1068         }
1069     }
1070 
1071     private void verifyPendingJobComparatorTransitivity() {
1072         mService.mPendingJobComparator.refreshLocked();
1073 
1074         for (int i = 0; i < mService.mPendingJobs.size(); ++i) {
1075             final JobStatus job1 = mService.mPendingJobs.get(i);
1076 
1077             for (int j = 0; j < mService.mPendingJobs.size(); ++j) {
1078                 final JobStatus job2 = mService.mPendingJobs.get(j);
1079                 final int sign12 = sign(mService.mPendingJobComparator.compare(job1, job2));
1080                 final int sign21 = sign(mService.mPendingJobComparator.compare(job2, job1));
1081                 if (sign12 != -sign21) {
1082                     final String job1String = sortedJobToString(job1);
1083                     final String job2String = sortedJobToString(job2);
1084                     fail("compare(" + job1String + ", " + job2String + ") != "
1085                             + "-compare(" + job2String + ", " + job1String + ")");
1086                 }
1087 
1088                 for (int k = 0; k < mService.mPendingJobs.size(); ++k) {
1089                     final JobStatus job3 = mService.mPendingJobs.get(k);
1090                     final int sign23 = sign(mService.mPendingJobComparator.compare(job2, job3));
1091                     final int sign13 = sign(mService.mPendingJobComparator.compare(job1, job3));
1092 
1093                     // Confirm 1 < 2 < 3 or 1 > 2 > 3 or 1 == 2 == 3
1094                     if ((sign12 == sign23 && sign12 != sign13)
1095                             // Confirm that if 1 == 2, then (1 < 3 AND 2 < 3) OR (1 > 3 && 2 > 3)
1096                             || (sign12 == 0 && sign13 != sign23)) {
1097                         final String job1String = sortedJobToString(job1);
1098                         final String job2String = sortedJobToString(job2);
1099                         final String job3String = sortedJobToString(job3);
1100                         fail("Transitivity fail"
1101                                 + ": compare(" + job1String + ", " + job2String + ")=" + sign12
1102                                 + ", compare(" + job2String + ", " + job3String + ")=" + sign23
1103                                 + ", compare(" + job1String + ", " + job3String + ")=" + sign13);
1104                     }
1105                 }
1106             }
1107         }
1108     }
1109 }
1110