1 package com.android.server.job;
2 
3 import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
4 import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
5 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
6 
7 import static org.junit.Assert.assertArrayEquals;
8 import static org.junit.Assert.assertEquals;
9 import static org.junit.Assert.assertFalse;
10 import static org.junit.Assert.assertNotNull;
11 import static org.junit.Assert.assertNull;
12 import static org.junit.Assert.assertThrows;
13 import static org.junit.Assert.assertTrue;
14 import static org.junit.Assert.fail;
15 import static org.mockito.ArgumentMatchers.anyString;
16 import static org.mockito.Mockito.mock;
17 import static org.mockito.Mockito.when;
18 
19 import android.app.job.JobInfo;
20 import android.app.job.JobInfo.Builder;
21 import android.app.job.JobWorkItem;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.pm.PackageManagerInternal;
25 import android.net.NetworkRequest;
26 import android.os.Build;
27 import android.os.PersistableBundle;
28 import android.os.SystemClock;
29 import android.test.RenamingDelegatingContext;
30 import android.util.ArraySet;
31 import android.util.Log;
32 import android.util.Pair;
33 
34 import androidx.test.InstrumentationRegistry;
35 import androidx.test.filters.SmallTest;
36 import androidx.test.runner.AndroidJUnit4;
37 
38 import com.android.internal.util.ArrayUtils;
39 import com.android.server.LocalServices;
40 import com.android.server.job.JobStore.JobSet;
41 import com.android.server.job.controllers.JobStatus;
42 
43 import org.junit.After;
44 import org.junit.Before;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 
48 import java.io.File;
49 import java.time.Clock;
50 import java.time.ZoneOffset;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Objects;
54 import java.util.Set;
55 
56 /**
57  * Test reading and writing correctly from file.
58  *
59  * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
60  */
61 @RunWith(AndroidJUnit4.class)
62 @SmallTest
63 public class JobStoreTest {
64     private static final String TAG = "TaskStoreTest";
65     private static final String TEST_PREFIX = "_test_";
66 
67     private static final int SOME_UID = android.os.Process.FIRST_APPLICATION_UID;
68     private ComponentName mComponent;
69 
70     JobStore mTaskStoreUnderTest;
71     Context mTestContext;
72 
getContext()73     private Context getContext() {
74         return InstrumentationRegistry.getContext();
75     }
76 
77     @Before
setUp()78     public void setUp() throws Exception {
79         mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
80         Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'");
81         mTaskStoreUnderTest =
82                 JobStore.initAndGetForTesting(mTestContext, mTestContext.getFilesDir());
83         mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
84 
85         // Assume all packages are current SDK
86         final PackageManagerInternal pm = mock(PackageManagerInternal.class);
87         when(pm.getPackageTargetSdkVersion(anyString()))
88                 .thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT);
89         LocalServices.removeServiceForTest(PackageManagerInternal.class);
90         LocalServices.addService(PackageManagerInternal.class, pm);
91 
92         // Freeze the clocks at this moment in time
93         JobSchedulerService.sSystemClock =
94                 Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
95         JobSchedulerService.sUptimeMillisClock =
96                 Clock.fixed(SystemClock.uptimeClock().instant(), ZoneOffset.UTC);
97         JobSchedulerService.sElapsedRealtimeClock =
98                 Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
99     }
100 
101     @After
tearDown()102     public void tearDown() throws Exception {
103         mTaskStoreUnderTest.clear();
104         mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L);
105     }
106 
setUseSplitFiles(boolean useSplitFiles)107     private void setUseSplitFiles(boolean useSplitFiles) throws Exception {
108         mTaskStoreUnderTest.setUseSplitFiles(useSplitFiles);
109         waitForPendingIo();
110     }
111 
waitForPendingIo()112     private void waitForPendingIo() throws Exception {
113         assertTrue("Timed out waiting for persistence I/O to complete",
114                 mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L));
115     }
116 
117     /** Test that we properly remove the last job of an app from the persisted file. */
118     @Test
testRemovingLastJob_singleFile()119     public void testRemovingLastJob_singleFile() throws Exception {
120         setUseSplitFiles(false);
121         runRemovingLastJob();
122     }
123 
124     /** Test that we properly remove the last job of an app from the persisted file. */
125     @Test
testRemovingLastJob_splitFiles()126     public void testRemovingLastJob_splitFiles() throws Exception {
127         setUseSplitFiles(true);
128         runRemovingLastJob();
129     }
130 
runRemovingLastJob()131     private void runRemovingLastJob() throws Exception {
132         final JobInfo task1 = new Builder(8, mComponent)
133                 .setRequiresDeviceIdle(true)
134                 .setPeriodic(10000L)
135                 .setRequiresCharging(true)
136                 .setPersisted(true)
137                 .build();
138         final JobInfo task2 = new Builder(12, mComponent)
139                 .setMinimumLatency(5000L)
140                 .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
141                 .setOverrideDeadline(30000L)
142                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
143                 .setPersisted(true)
144                 .build();
145         final int uid1 = SOME_UID;
146         final int uid2 = uid1 + 1;
147         final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
148         final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
149         runWritingJobsToDisk(JobStatus1, JobStatus2);
150 
151         // Remove 1 job
152         mTaskStoreUnderTest.remove(JobStatus1, true);
153         waitForPendingIo();
154         JobSet jobStatusSet = new JobSet();
155         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
156         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
157         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
158 
159         assertJobsEqual(JobStatus2, loaded);
160         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(JobStatus2));
161 
162         // Remove 2nd job
163         mTaskStoreUnderTest.remove(JobStatus2, true);
164         waitForPendingIo();
165         jobStatusSet = new JobSet();
166         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
167         assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
168     }
169 
170     /** Test that we properly clear the persisted file when all jobs are dropped. */
171     @Test
testClearJobs_singleFile()172     public void testClearJobs_singleFile() throws Exception {
173         setUseSplitFiles(false);
174         runClearJobs();
175     }
176 
177     /** Test that we properly clear the persisted file when all jobs are dropped. */
178     @Test
testClearJobs_splitFiles()179     public void testClearJobs_splitFiles() throws Exception {
180         setUseSplitFiles(true);
181         runClearJobs();
182     }
183 
runClearJobs()184     private void runClearJobs() throws Exception {
185         final JobInfo task1 = new Builder(8, mComponent)
186                 .setRequiresDeviceIdle(true)
187                 .setPeriodic(10000L)
188                 .setRequiresCharging(true)
189                 .setPersisted(true)
190                 .build();
191         final JobInfo task2 = new Builder(12, mComponent)
192                 .setMinimumLatency(5000L)
193                 .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
194                 .setOverrideDeadline(30000L)
195                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
196                 .setPersisted(true)
197                 .build();
198         final int uid1 = SOME_UID;
199         final int uid2 = uid1 + 1;
200         final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
201         final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
202         runWritingJobsToDisk(JobStatus1, JobStatus2);
203 
204         // Remove all jobs
205         mTaskStoreUnderTest.clear();
206         waitForPendingIo();
207         JobSet jobStatusSet = new JobSet();
208         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
209         assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size());
210     }
211 
212     /**
213      * Test that dynamic constraints aren't written to disk.
214      */
215     @Test
testDynamicConstraintsNotPersisted()216     public void testDynamicConstraintsNotPersisted() throws Exception {
217         JobInfo.Builder b = new Builder(42, mComponent).setPersisted(true);
218         JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
219         js.addDynamicConstraints(JobStatus.CONSTRAINT_BATTERY_NOT_LOW
220                 | JobStatus.CONSTRAINT_CHARGING
221                 | JobStatus.CONSTRAINT_IDLE
222                 | JobStatus.CONSTRAINT_STORAGE_NOT_LOW);
223         assertTrue(js.hasBatteryNotLowConstraint());
224         assertTrue(js.hasChargingConstraint());
225         assertTrue(js.hasIdleConstraint());
226         assertTrue(js.hasStorageNotLowConstraint());
227         mTaskStoreUnderTest.add(js);
228         waitForPendingIo();
229 
230         final JobSet jobStatusSet = new JobSet();
231         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
232         assertEquals("Job count is incorrect.", 1, jobStatusSet.size());
233         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
234         assertFalse(loaded.hasBatteryNotLowConstraint());
235         assertFalse(loaded.hasChargingConstraint());
236         assertFalse(loaded.hasIdleConstraint());
237         assertFalse(loaded.hasStorageNotLowConstraint());
238     }
239 
240     @Test
testExtractUidFromJobFileName()241     public void testExtractUidFromJobFileName() {
242         File file = new File(mTestContext.getFilesDir(), "randomName");
243         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
244 
245         file = new File(mTestContext.getFilesDir(), "jobs.xml");
246         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
247 
248         file = new File(mTestContext.getFilesDir(), ".xml");
249         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
250 
251         file = new File(mTestContext.getFilesDir(), "1000.xml");
252         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
253 
254         file = new File(mTestContext.getFilesDir(), "10000");
255         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
256 
257         file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX);
258         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
259 
260         file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "text.xml");
261         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
262 
263         file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + ".xml");
264         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
265 
266         file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "-10123.xml");
267         assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file));
268 
269         file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "1.xml");
270         assertEquals(1, JobStore.extractUidFromJobFileName(file));
271 
272         file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "101023.xml");
273         assertEquals(101023, JobStore.extractUidFromJobFileName(file));
274     }
275 
276     @Test
testStringToIntArrayAndIntArrayToString()277     public void testStringToIntArrayAndIntArrayToString() {
278         final int[] netCapabilitiesIntArray = { 1, 3, 5, 7, 9 };
279         final String netCapabilitiesStr = "1,3,5,7,9";
280         final String netCapabilitiesStrWithErrorInt = "1,3,a,7,9";
281         final String emptyString = "";
282         final String str1 = JobStore.intArrayToString(netCapabilitiesIntArray);
283         assertArrayEquals(netCapabilitiesIntArray, JobStore.stringToIntArray(str1));
284         assertEquals(0, JobStore.stringToIntArray(emptyString).length);
285         assertThrows(NumberFormatException.class,
286                 () -> JobStore.stringToIntArray(netCapabilitiesStrWithErrorInt));
287         assertEquals(netCapabilitiesStr, JobStore.intArrayToString(netCapabilitiesIntArray));
288     }
289 
290     @Test
testMaybeWriteStatusToDisk()291     public void testMaybeWriteStatusToDisk() throws Exception {
292         int taskId = 5;
293         long runByMillis = 20000L; // 20s
294         long runFromMillis = 2000L; // 2s
295         long initialBackoff = 10000L; // 10s
296 
297         final JobInfo task = new Builder(taskId, mComponent)
298                 .setRequiresCharging(true)
299                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
300                 .setBackoffCriteria(initialBackoff, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
301                 .setOverrideDeadline(runByMillis)
302                 .setMinimumLatency(runFromMillis)
303                 .setPersisted(true)
304                 .build();
305         final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null, null);
306         ts.addInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION);
307         mTaskStoreUnderTest.add(ts);
308         waitForPendingIo();
309 
310         // Manually load tasks from xml file.
311         final JobSet jobStatusSet = new JobSet();
312         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
313 
314         assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size());
315         final JobStatus loadedTaskStatus = jobStatusSet.getAllJobs().get(0);
316         assertJobsEqual(ts, loadedTaskStatus);
317         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(ts));
318     }
319 
320     @Test
testWritingTwoJobsToDisk_singleFile()321     public void testWritingTwoJobsToDisk_singleFile() throws Exception {
322         setUseSplitFiles(false);
323         runWritingTwoJobsToDisk();
324     }
325 
326     @Test
testWritingTwoJobsToDisk_splitFiles()327     public void testWritingTwoJobsToDisk_splitFiles() throws Exception {
328         setUseSplitFiles(true);
329         runWritingTwoJobsToDisk();
330     }
331 
runWritingTwoJobsToDisk()332     private void runWritingTwoJobsToDisk() throws Exception {
333         final JobInfo task1 = new Builder(8, mComponent)
334                 .setRequiresDeviceIdle(true)
335                 .setPeriodic(10000L)
336                 .setRequiresCharging(true)
337                 .setPersisted(true)
338                 .build();
339         final JobInfo task2 = new Builder(12, mComponent)
340                 .setMinimumLatency(5000L)
341                 .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
342                 .setOverrideDeadline(30000L)
343                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
344                 .setPersisted(true)
345                 .build();
346         final int uid1 = SOME_UID;
347         final int uid2 = uid1 + 1;
348         final JobStatus taskStatus1 =
349                 JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
350         final JobStatus taskStatus2 =
351                 JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
352 
353         runWritingJobsToDisk(taskStatus1, taskStatus2);
354     }
355 
runWritingJobsToDisk(JobStatus... jobStatuses)356     private void runWritingJobsToDisk(JobStatus... jobStatuses) throws Exception {
357         ArraySet<JobStatus> expectedJobs = new ArraySet<>();
358         for (JobStatus jobStatus : jobStatuses) {
359             mTaskStoreUnderTest.add(jobStatus);
360             expectedJobs.add(jobStatus);
361         }
362         waitForPendingIo();
363 
364         final JobSet jobStatusSet = new JobSet();
365         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
366         assertEquals("Incorrect # of persisted tasks.", expectedJobs.size(), jobStatusSet.size());
367         int count = 0;
368         final int expectedCount = expectedJobs.size();
369         for (JobStatus loaded : jobStatusSet.getAllJobs()) {
370             count++;
371             for (int i = 0; i < expectedJobs.size(); ++i) {
372                 JobStatus expected = expectedJobs.valueAt(i);
373 
374                 try {
375                     assertJobsEqual(expected, loaded);
376                     expectedJobs.remove(expected);
377                     break;
378                 } catch (AssertionError e) {
379                     // Not equal. Move along.
380                 }
381             }
382         }
383         assertEquals("Loaded more jobs than expected", expectedCount, count);
384         if (expectedJobs.size() > 0) {
385             fail("Not all expected jobs were restored");
386         }
387         for (JobStatus jobStatus : jobStatuses) {
388             assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(jobStatus));
389         }
390     }
391 
392     @Test
testWritingTaskWithExtras()393     public void testWritingTaskWithExtras() throws Exception {
394         JobInfo.Builder b = new Builder(8, mComponent)
395                 .setRequiresDeviceIdle(true)
396                 .setPeriodic(10000L)
397                 .setRequiresCharging(true)
398                 .setPersisted(true);
399 
400         PersistableBundle extras = new PersistableBundle();
401         extras.putDouble("hello", 3.2);
402         extras.putString("hi", "there");
403         extras.putInt("into", 3);
404         b.setExtras(extras);
405         final JobInfo task = b.build();
406         JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null, null);
407 
408         mTaskStoreUnderTest.add(taskStatus);
409         waitForPendingIo();
410 
411         final JobSet jobStatusSet = new JobSet();
412         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
413         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
414         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
415         assertJobsEqual(taskStatus, loaded);
416     }
417 
418     @Test
testWritingTaskWithSourcePackage()419     public void testWritingTaskWithSourcePackage() throws Exception {
420         JobInfo.Builder b = new Builder(8, mComponent)
421                 .setRequiresDeviceIdle(true)
422                 .setPeriodic(10000L)
423                 .setRequiresCharging(true)
424                 .setPersisted(true);
425         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID,
426                 "com.android.test.app", 0, null, null);
427 
428         mTaskStoreUnderTest.add(taskStatus);
429         waitForPendingIo();
430 
431         final JobSet jobStatusSet = new JobSet();
432         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
433         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
434         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
435         assertEquals("Source package not equal.", loaded.getSourcePackageName(),
436                 taskStatus.getSourcePackageName());
437         assertEquals("Source user not equal.", loaded.getSourceUserId(),
438                 taskStatus.getSourceUserId());
439     }
440 
441     @Test
testWritingTaskWithFlex()442     public void testWritingTaskWithFlex() throws Exception {
443         JobInfo.Builder b = new Builder(8, mComponent)
444                 .setRequiresDeviceIdle(true)
445                 .setPeriodic(5*60*60*1000, 1*60*60*1000)
446                 .setRequiresCharging(true)
447                 .setPersisted(true);
448         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
449                 SOME_UID, null, -1, null, null);
450 
451         mTaskStoreUnderTest.add(taskStatus);
452         waitForPendingIo();
453 
454         final JobSet jobStatusSet = new JobSet();
455         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
456         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
457         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
458         assertEquals("Period not equal.", loaded.getJob().getIntervalMillis(),
459                 taskStatus.getJob().getIntervalMillis());
460         assertEquals("Flex not equal.", loaded.getJob().getFlexMillis(),
461                 taskStatus.getJob().getFlexMillis());
462     }
463 
464     @Test
testMassivePeriodClampedOnRead()465     public void testMassivePeriodClampedOnRead() throws Exception {
466         final long ONE_HOUR = 60*60*1000L; // flex
467         final long TWO_HOURS = 2 * ONE_HOUR; // period
468         JobInfo.Builder b = new Builder(8, mComponent)
469                 .setPeriodic(TWO_HOURS, ONE_HOUR)
470                 .setPersisted(true);
471         final long rtcNow = System.currentTimeMillis();
472         final long invalidLateRuntimeElapsedMillis =
473                 SystemClock.elapsedRealtime() + (TWO_HOURS * ONE_HOUR) + TWO_HOURS;  // > period+flex
474         final long invalidEarlyRuntimeElapsedMillis =
475                 invalidLateRuntimeElapsedMillis - TWO_HOURS;  // Early is (late - period).
476         final Pair<Long, Long> persistedExecutionTimesUTC = new Pair<>(rtcNow, rtcNow + ONE_HOUR);
477         final JobStatus js = new JobStatus(b.build(), SOME_UID, "somePackage",
478                 0 /* sourceUserId */, 0, "someNamespace", "someTag",
479                 invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis,
480                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
481                 0 /* cumulativeExecutionTime */,
482                 persistedExecutionTimesUTC, 0 /* innerFlag */, 0 /* dynamicConstraints */);
483 
484         mTaskStoreUnderTest.add(js);
485         waitForPendingIo();
486 
487         final JobSet jobStatusSet = new JobSet();
488         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
489         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
490         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
491 
492         // Assert early runtime was clamped to be under now + period. We can do <= here b/c we'll
493         // call SystemClock.elapsedRealtime after doing the disk i/o.
494         final long newNowElapsed = SystemClock.elapsedRealtime();
495         assertTrue("Early runtime wasn't correctly clamped.",
496                 loaded.getEarliestRunTime() <= newNowElapsed + TWO_HOURS);
497         // Assert late runtime was clamped to be now + period + flex.
498         assertTrue("Early runtime wasn't correctly clamped.",
499                 loaded.getEarliestRunTime() <= newNowElapsed + TWO_HOURS + ONE_HOUR);
500     }
501 
502     @Test
testBiasPersisted()503     public void testBiasPersisted() throws Exception {
504         JobInfo.Builder b = new Builder(92, mComponent)
505                 .setOverrideDeadline(5000)
506                 .setBias(42)
507                 .setPersisted(true);
508         final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
509         mTaskStoreUnderTest.add(js);
510         waitForPendingIo();
511 
512         final JobSet jobStatusSet = new JobSet();
513         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
514         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
515         assertEquals("Bias not correctly persisted.", 42, loaded.getBias());
516     }
517 
518     @Test
testCumulativeExecutionTimePersisted()519     public void testCumulativeExecutionTimePersisted() throws Exception {
520         JobInfo ji = new Builder(53, mComponent).setPersisted(true).build();
521         final JobStatus js = JobStatus.createFromJobInfo(ji, SOME_UID, null, -1, null, null);
522         js.incrementCumulativeExecutionTime(1234567890);
523         mTaskStoreUnderTest.add(js);
524         waitForPendingIo();
525 
526         final JobSet jobStatusSet = new JobSet();
527         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
528         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
529         assertEquals("Cumulative execution time not correctly persisted.",
530                 1234567890, loaded.getCumulativeExecutionTimeMs());
531     }
532 
533     @Test
testNamespacePersisted()534     public void testNamespacePersisted() throws Exception {
535         final String namespace = "my.test.namespace";
536         JobInfo.Builder b = new Builder(93, mComponent)
537                 .setRequiresBatteryNotLow(true)
538                 .setPersisted(true);
539         final JobStatus js =
540                 JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, namespace, null);
541         mTaskStoreUnderTest.add(js);
542         waitForPendingIo();
543 
544         final JobSet jobStatusSet = new JobSet();
545         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
546         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
547         assertEquals("Namespace not correctly persisted.", namespace, loaded.getNamespace());
548     }
549 
550     @Test
testPriorityPersisted()551     public void testPriorityPersisted() throws Exception {
552         final JobInfo job = new Builder(92, mComponent)
553                 .setOverrideDeadline(5000)
554                 .setPriority(JobInfo.PRIORITY_MIN)
555                 .setPersisted(true)
556                 .build();
557         final JobStatus js = JobStatus.createFromJobInfo(job, SOME_UID, null, -1, null, null);
558         mTaskStoreUnderTest.add(js);
559         waitForPendingIo();
560 
561         final JobSet jobStatusSet = new JobSet();
562         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
563         final JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
564         assertEquals("Priority not correctly persisted.",
565                 JobInfo.PRIORITY_MIN, job.getPriority());
566     }
567 
568     /**
569      * Test that non persisted job is not written to disk.
570      */
571     @Test
testNonPersistedTaskIsNotPersisted()572     public void testNonPersistedTaskIsNotPersisted() throws Exception {
573         JobInfo.Builder b = new Builder(42, mComponent)
574                 .setOverrideDeadline(10000)
575                 .setPersisted(false);
576         JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(),
577                 SOME_UID, null, -1, null, null);
578         mTaskStoreUnderTest.add(jsNonPersisted);
579         b = new Builder(43, mComponent)
580                 .setOverrideDeadline(10000)
581                 .setPersisted(true);
582         JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(),
583                 SOME_UID, null, -1, null, null);
584         mTaskStoreUnderTest.add(jsPersisted);
585         waitForPendingIo();
586 
587         final JobSet jobStatusSet = new JobSet();
588         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
589         assertEquals("Job count is incorrect.", 1, jobStatusSet.size());
590         JobStatus jobStatus = jobStatusSet.getAllJobs().iterator().next();
591         assertEquals("Wrong job persisted.", 43, jobStatus.getJobId());
592     }
593 
594     @Test
testRequiredNetworkType()595     public void testRequiredNetworkType() throws Exception {
596         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
597                 .setPersisted(true)
598                 .setRequiresDeviceIdle(true)
599                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE).build());
600         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
601                 .setPersisted(true)
602                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build());
603         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
604                 .setPersisted(true)
605                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED).build());
606         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
607                 .setPersisted(true)
608                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING).build());
609         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
610                 .setPersisted(true)
611                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_CELLULAR).build());
612     }
613 
614     @Test
testRequiredNetwork()615     public void testRequiredNetwork() throws Exception {
616         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
617                 .setPersisted(true)
618                 .setRequiresDeviceIdle(true)
619                 .setRequiredNetwork(null).build());
620         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
621                 .setPersisted(true)
622                 .setRequiredNetwork(new NetworkRequest.Builder().build()).build());
623         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
624                 .setPersisted(true)
625                 .setRequiredNetwork(new NetworkRequest.Builder()
626                         .addTransportType(TRANSPORT_WIFI).build())
627                 .build());
628         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
629                 .setPersisted(true)
630                 .setRequiredNetwork(new NetworkRequest.Builder()
631                         .addCapability(NET_CAPABILITY_IMS)
632                         .addForbiddenCapability(NET_CAPABILITY_OEM_PAID)
633                         .build())
634                 .build());
635     }
636 
637     @Test
testEstimatedNetworkBytes()638     public void testEstimatedNetworkBytes() throws Exception {
639         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
640                 .setPersisted(true)
641                 .setRequiredNetwork(new NetworkRequest.Builder().build())
642                 .setEstimatedNetworkBytes(
643                         JobInfo.NETWORK_BYTES_UNKNOWN, JobInfo.NETWORK_BYTES_UNKNOWN)
644                 .build());
645         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
646                 .setPersisted(true)
647                 .setRequiredNetwork(new NetworkRequest.Builder().build())
648                 .setEstimatedNetworkBytes(5, 15)
649                 .build());
650     }
651 
652     @Test
testMinimumNetworkChunkBytes()653     public void testMinimumNetworkChunkBytes() throws Exception {
654         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
655                 .setPersisted(true)
656                 .setRequiredNetwork(new NetworkRequest.Builder().build())
657                 .setMinimumNetworkChunkBytes(JobInfo.NETWORK_BYTES_UNKNOWN)
658                 .build());
659         assertPersistedEquals(new JobInfo.Builder(0, mComponent)
660                 .setPersisted(true)
661                 .setRequiredNetwork(new NetworkRequest.Builder().build())
662                 .setMinimumNetworkChunkBytes(42)
663                 .build());
664     }
665 
666     @Test
testPersistedIdleConstraint()667     public void testPersistedIdleConstraint() throws Exception {
668         JobInfo.Builder b = new Builder(8, mComponent)
669                 .setRequiresDeviceIdle(true)
670                 .setPersisted(true);
671         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
672                 SOME_UID, null, -1, null, null);
673 
674         mTaskStoreUnderTest.add(taskStatus);
675         waitForPendingIo();
676 
677         final JobSet jobStatusSet = new JobSet();
678         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
679         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
680         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
681         assertEquals("Idle constraint not persisted correctly.",
682                 loaded.getJob().isRequireDeviceIdle(),
683                 taskStatus.getJob().isRequireDeviceIdle());
684     }
685 
686     @Test
testPersistedChargingConstraint()687     public void testPersistedChargingConstraint() throws Exception {
688         JobInfo.Builder b = new Builder(8, mComponent)
689                 .setRequiresCharging(true)
690                 .setPersisted(true);
691         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
692                 SOME_UID, null, -1, null, null);
693 
694         mTaskStoreUnderTest.add(taskStatus);
695         waitForPendingIo();
696 
697         final JobSet jobStatusSet = new JobSet();
698         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
699         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
700         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
701         assertEquals("Charging constraint not persisted correctly.",
702                 loaded.getJob().isRequireCharging(),
703                 taskStatus.getJob().isRequireCharging());
704     }
705 
706     @Test
testPersistedStorageNotLowConstraint()707     public void testPersistedStorageNotLowConstraint() throws Exception {
708         JobInfo.Builder b = new Builder(8, mComponent)
709                 .setRequiresStorageNotLow(true)
710                 .setPersisted(true);
711         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
712                 SOME_UID, null, -1, null, null);
713 
714         mTaskStoreUnderTest.add(taskStatus);
715         waitForPendingIo();
716 
717         final JobSet jobStatusSet = new JobSet();
718         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
719         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
720         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
721         assertEquals("Storage-not-low constraint not persisted correctly.",
722                 loaded.getJob().isRequireStorageNotLow(),
723                 taskStatus.getJob().isRequireStorageNotLow());
724     }
725 
726     @Test
testPersistedBatteryNotLowConstraint()727     public void testPersistedBatteryNotLowConstraint() throws Exception {
728         JobInfo.Builder b = new Builder(8, mComponent)
729                 .setRequiresBatteryNotLow(true)
730                 .setPersisted(true);
731         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
732                 SOME_UID, null, -1, null, null);
733 
734         mTaskStoreUnderTest.add(taskStatus);
735         waitForPendingIo();
736 
737         final JobSet jobStatusSet = new JobSet();
738         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
739         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
740         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
741         assertEquals("Battery-not-low constraint not persisted correctly.",
742                 loaded.getJob().isRequireBatteryNotLow(),
743                 taskStatus.getJob().isRequireBatteryNotLow());
744     }
745 
746     @Test
testPersistedPreferredBatteryNotLowConstraint()747     public void testPersistedPreferredBatteryNotLowConstraint() throws Exception {
748         JobInfo.Builder b = new Builder(8, mComponent)
749                 .setPrefersBatteryNotLow(true)
750                 .setPersisted(true);
751         JobStatus taskStatus =
752                 JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
753 
754         mTaskStoreUnderTest.add(taskStatus);
755         waitForPendingIo();
756 
757         final JobSet jobStatusSet = new JobSet();
758         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
759         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
760         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
761         assertEquals("Battery-not-low constraint not persisted correctly.",
762                 taskStatus.getJob().isPreferBatteryNotLow(),
763                 loaded.getJob().isPreferBatteryNotLow());
764     }
765 
766     @Test
testPersistedPreferredChargingConstraint()767     public void testPersistedPreferredChargingConstraint() throws Exception {
768         JobInfo.Builder b = new Builder(8, mComponent)
769                 .setPrefersCharging(true)
770                 .setPersisted(true);
771         JobStatus taskStatus =
772                 JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
773 
774         mTaskStoreUnderTest.add(taskStatus);
775         waitForPendingIo();
776 
777         final JobSet jobStatusSet = new JobSet();
778         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
779         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
780         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
781         assertEquals("Charging constraint not persisted correctly.",
782                 taskStatus.getJob().isPreferCharging(),
783                 loaded.getJob().isPreferCharging());
784     }
785 
786     @Test
testPersistedPreferredDeviceIdleConstraint()787     public void testPersistedPreferredDeviceIdleConstraint() throws Exception {
788         JobInfo.Builder b = new Builder(8, mComponent)
789                 .setPrefersDeviceIdle(true)
790                 .setPersisted(true);
791         JobStatus taskStatus =
792                 JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
793 
794         mTaskStoreUnderTest.add(taskStatus);
795         waitForPendingIo();
796 
797         final JobSet jobStatusSet = new JobSet();
798         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
799         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
800         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
801         assertEquals("Idle constraint not persisted correctly.",
802                 taskStatus.getJob().isPreferDeviceIdle(),
803                 loaded.getJob().isPreferDeviceIdle());
804     }
805 
806     @Test
testJobWorkItems()807     public void testJobWorkItems() throws Exception {
808         JobWorkItem item1 = new JobWorkItem.Builder().build();
809         item1.bumpDeliveryCount();
810         PersistableBundle bundle = new PersistableBundle();
811         bundle.putBoolean("test", true);
812         JobWorkItem item2 = new JobWorkItem.Builder().setExtras(bundle).build();
813         item2.bumpDeliveryCount();
814         JobWorkItem item3 = new JobWorkItem.Builder().setEstimatedNetworkBytes(1, 2).build();
815         JobWorkItem item4 = new JobWorkItem.Builder().setMinimumNetworkChunkBytes(3).build();
816         JobWorkItem item5 = new JobWorkItem.Builder().build();
817 
818         JobInfo jobInfo = new JobInfo.Builder(0, mComponent)
819                 .setPersisted(true)
820                 .build();
821         JobStatus jobStatus =
822                 JobStatus.createFromJobInfo(jobInfo, SOME_UID, null, -1, null, null);
823         jobStatus.executingWork = new ArrayList<>(List.of(item1, item2));
824         jobStatus.pendingWork = new ArrayList<>(List.of(item3, item4, item5));
825         assertPersistedEquals(jobStatus);
826     }
827 
828     /**
829      * Helper function to kick a {@link JobInfo} through a persistence cycle and
830      * assert that it's unchanged.
831      */
assertPersistedEquals(JobInfo firstInfo)832     private void assertPersistedEquals(JobInfo firstInfo) throws Exception {
833         assertPersistedEquals(
834                 JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null, null));
835     }
836 
assertPersistedEquals(JobStatus original)837     private void assertPersistedEquals(JobStatus original) throws Exception {
838         mTaskStoreUnderTest.clear();
839         mTaskStoreUnderTest.add(original);
840         waitForPendingIo();
841 
842         final JobSet jobStatusSet = new JobSet();
843         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
844         final JobStatus second = jobStatusSet.getAllJobs().iterator().next();
845         assertJobsEqual(original, second);
846     }
847 
848     /**
849      * Helper function to throw an error if the provided JobStatus objects are not equal.
850      */
assertJobsEqual(JobStatus expected, JobStatus actual)851     private void assertJobsEqual(JobStatus expected, JobStatus actual) {
852         assertEquals(expected.getJob(), actual.getJob());
853 
854         // Source UID isn't persisted, but the rest of the app info is.
855         assertEquals("Source package not equal",
856                 expected.getSourcePackageName(), actual.getSourcePackageName());
857         assertEquals("Source user not equal", expected.getSourceUserId(), actual.getSourceUserId());
858         assertEquals("Calling UID not equal", expected.getUid(), actual.getUid());
859         assertEquals("Calling user not equal", expected.getUserId(), actual.getUserId());
860 
861         assertEquals(expected.getNamespace(), actual.getNamespace());
862 
863         assertEquals("Internal flags not equal",
864                 expected.getInternalFlags(), actual.getInternalFlags());
865 
866         // Check that the loaded task has the correct runtimes.
867         compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
868                 expected.getEarliestRunTime(), actual.getEarliestRunTime());
869         compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
870                 expected.getLatestRunTimeElapsed(), actual.getLatestRunTimeElapsed());
871 
872         assertEquals(expected.getCumulativeExecutionTimeMs(),
873                 actual.getCumulativeExecutionTimeMs());
874 
875         assertEquals(expected.hasWorkLocked(), actual.hasWorkLocked());
876         if (expected.hasWorkLocked()) {
877             List<JobWorkItem> allWork = new ArrayList<>();
878             if (expected.executingWork != null) {
879                 allWork.addAll(expected.executingWork);
880             }
881             if (expected.pendingWork != null) {
882                 allWork.addAll(expected.pendingWork);
883             }
884             // All work for freshly loaded Job will be pending.
885             assertNotNull(actual.pendingWork);
886             assertTrue(ArrayUtils.isEmpty(actual.executingWork));
887             assertEquals(allWork.size(), actual.pendingWork.size());
888             for (int i = 0; i < allWork.size(); ++i) {
889                 JobWorkItem expectedItem = allWork.get(i);
890                 JobWorkItem actualItem = actual.pendingWork.get(i);
891                 assertJobWorkItemsEqual(expectedItem, actualItem);
892             }
893         }
894     }
895 
assertJobWorkItemsEqual(JobWorkItem expected, JobWorkItem actual)896     private void assertJobWorkItemsEqual(JobWorkItem expected, JobWorkItem actual) {
897         if (expected == null) {
898             assertNull(actual);
899             return;
900         }
901         assertNotNull(actual);
902         assertEquals(expected.getDeliveryCount(), actual.getDeliveryCount());
903         assertEquals(expected.getEstimatedNetworkDownloadBytes(),
904                 actual.getEstimatedNetworkDownloadBytes());
905         assertEquals(expected.getEstimatedNetworkUploadBytes(),
906                 actual.getEstimatedNetworkUploadBytes());
907         assertEquals(expected.getMinimumNetworkChunkBytes(), actual.getMinimumNetworkChunkBytes());
908         if (expected.getIntent() == null) {
909             assertNull(actual.getIntent());
910         } else {
911             // filterEquals() just so happens to check almost everything that is persisted to disk.
912             assertTrue(expected.getIntent().filterEquals(actual.getIntent()));
913             assertEquals(expected.getIntent().getFlags(), actual.getIntent().getFlags());
914         }
915         assertEquals(expected.getGrants(), actual.getGrants());
916         PersistableBundle expectedExtras = expected.getExtras();
917         PersistableBundle actualExtras = actual.getExtras();
918         if (expectedExtras == null) {
919             assertNull(actualExtras);
920         } else {
921             assertEquals(expectedExtras.size(), actualExtras.size());
922             Set<String> keys = expectedExtras.keySet();
923             for (String key : keys) {
924                 assertTrue(Objects.equals(expectedExtras.get(key), actualExtras.get(key)));
925             }
926         }
927     }
928 
929     /**
930      * When comparing timestamps before and after DB read/writes (to make sure we're saving/loading
931      * the correct values), there is some latency involved that terrorises a naive assertEquals().
932      * We define a <code>DELTA_MILLIS</code> as a function variable here to make this comparision
933      * more reasonable.
934      */
compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2)935     private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) {
936         final long DELTA_MILLIS = 700L;  // We allow up to 700ms of latency for IO read/writes.
937         assertTrue(error, Math.abs(ts1 - ts2) < DELTA_MILLIS);
938     }
939 
940     private static class StubClass {}
941 }
942