1 package com.android.server.job;
2 
3 
4 import android.content.ComponentName;
5 import android.content.Context;
6 import android.app.job.JobInfo;
7 import android.app.job.JobInfo.Builder;
8 import android.os.PersistableBundle;
9 import android.os.SystemClock;
10 import android.test.AndroidTestCase;
11 import android.test.RenamingDelegatingContext;
12 import android.util.Log;
13 import android.util.ArraySet;
14 
15 import com.android.server.job.JobStore.JobSet;
16 import com.android.server.job.controllers.JobStatus;
17 
18 import java.util.Iterator;
19 
20 /**
21  * Test reading and writing correctly from file.
22  */
23 public class JobStoreTest extends AndroidTestCase {
24     private static final String TAG = "TaskStoreTest";
25     private static final String TEST_PREFIX = "_test_";
26 
27     private static final int SOME_UID = 34234;
28     private ComponentName mComponent;
29     private static final long IO_WAIT = 1000L;
30 
31     JobStore mTaskStoreUnderTest;
32     Context mTestContext;
33 
34     @Override
setUp()35     public void setUp() throws Exception {
36         mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
37         Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'");
38         mTaskStoreUnderTest =
39                 JobStore.initAndGetForTesting(mTestContext, mTestContext.getFilesDir());
40         mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
41     }
42 
43     @Override
tearDown()44     public void tearDown() throws Exception {
45         mTaskStoreUnderTest.clear();
46     }
47 
testMaybeWriteStatusToDisk()48     public void testMaybeWriteStatusToDisk() throws Exception {
49         int taskId = 5;
50         long runByMillis = 20000L; // 20s
51         long runFromMillis = 2000L; // 2s
52         long initialBackoff = 10000L; // 10s
53 
54         final JobInfo task = new Builder(taskId, mComponent)
55                 .setRequiresCharging(true)
56                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
57                 .setBackoffCriteria(initialBackoff, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
58                 .setOverrideDeadline(runByMillis)
59                 .setMinimumLatency(runFromMillis)
60                 .setPersisted(true)
61                 .build();
62         final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null);
63         mTaskStoreUnderTest.add(ts);
64         Thread.sleep(IO_WAIT);
65         // Manually load tasks from xml file.
66         final JobSet jobStatusSet = new JobSet();
67         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
68 
69         assertEquals("Didn't get expected number of persisted tasks.", 1, jobStatusSet.size());
70         final JobStatus loadedTaskStatus = jobStatusSet.getAllJobs().get(0);
71         assertTasksEqual(task, loadedTaskStatus.getJob());
72         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(ts));
73         assertEquals("Different uids.", SOME_UID, loadedTaskStatus.getUid());
74         compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
75                 ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
76         compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
77                 ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
78 
79     }
80 
testWritingTwoFilesToDisk()81     public void testWritingTwoFilesToDisk() throws Exception {
82         final JobInfo task1 = new Builder(8, mComponent)
83                 .setRequiresDeviceIdle(true)
84                 .setPeriodic(10000L)
85                 .setRequiresCharging(true)
86                 .setPersisted(true)
87                 .build();
88         final JobInfo task2 = new Builder(12, mComponent)
89                 .setMinimumLatency(5000L)
90                 .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR)
91                 .setOverrideDeadline(30000L)
92                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
93                 .setPersisted(true)
94                 .build();
95         final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, SOME_UID, null, -1, null);
96         final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, SOME_UID, null, -1, null);
97         mTaskStoreUnderTest.add(taskStatus1);
98         mTaskStoreUnderTest.add(taskStatus2);
99         Thread.sleep(IO_WAIT);
100 
101         final JobSet jobStatusSet = new JobSet();
102         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
103         assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size());
104         Iterator<JobStatus> it = jobStatusSet.getAllJobs().iterator();
105         JobStatus loaded1 = it.next();
106         JobStatus loaded2 = it.next();
107 
108         // Reverse them so we know which comparison to make.
109         if (loaded1.getJobId() != 8) {
110             JobStatus tmp = loaded1;
111             loaded1 = loaded2;
112             loaded2 = tmp;
113         }
114 
115         assertTasksEqual(task1, loaded1.getJob());
116         assertTasksEqual(task2, loaded2.getJob());
117         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1));
118         assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2));
119         // Check that the loaded task has the correct runtimes.
120         compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
121                 taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
122         compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
123                 taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
124         compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
125                 taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
126         compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
127                 taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
128 
129     }
130 
testWritingTaskWithExtras()131     public void testWritingTaskWithExtras() throws Exception {
132         JobInfo.Builder b = new Builder(8, mComponent)
133                 .setRequiresDeviceIdle(true)
134                 .setPeriodic(10000L)
135                 .setRequiresCharging(true)
136                 .setPersisted(true);
137 
138         PersistableBundle extras = new PersistableBundle();
139         extras.putDouble("hello", 3.2);
140         extras.putString("hi", "there");
141         extras.putInt("into", 3);
142         b.setExtras(extras);
143         final JobInfo task = b.build();
144         JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null);
145 
146         mTaskStoreUnderTest.add(taskStatus);
147         Thread.sleep(IO_WAIT);
148 
149         final JobSet jobStatusSet = new JobSet();
150         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
151         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
152         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
153         assertTasksEqual(task, loaded.getJob());
154     }
testWritingTaskWithSourcePackage()155     public void testWritingTaskWithSourcePackage() throws Exception {
156         JobInfo.Builder b = new Builder(8, mComponent)
157                 .setRequiresDeviceIdle(true)
158                 .setPeriodic(10000L)
159                 .setRequiresCharging(true)
160                 .setPersisted(true);
161         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID,
162                 "com.google.android.gms", 0, null);
163 
164         mTaskStoreUnderTest.add(taskStatus);
165         Thread.sleep(IO_WAIT);
166 
167         final JobSet jobStatusSet = new JobSet();
168         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
169         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
170         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
171         assertEquals("Source package not equal.", loaded.getSourcePackageName(),
172                 taskStatus.getSourcePackageName());
173         assertEquals("Source user not equal.", loaded.getSourceUserId(),
174                 taskStatus.getSourceUserId());
175     }
176 
testWritingTaskWithFlex()177     public void testWritingTaskWithFlex() throws Exception {
178         JobInfo.Builder b = new Builder(8, mComponent)
179                 .setRequiresDeviceIdle(true)
180                 .setPeriodic(5*60*60*1000, 1*60*60*1000)
181                 .setRequiresCharging(true)
182                 .setPersisted(true);
183         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
184 
185         mTaskStoreUnderTest.add(taskStatus);
186         Thread.sleep(IO_WAIT);
187 
188         final JobSet jobStatusSet = new JobSet();
189         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
190         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
191         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
192         assertEquals("Period not equal.", loaded.getJob().getIntervalMillis(),
193                 taskStatus.getJob().getIntervalMillis());
194         assertEquals("Flex not equal.", loaded.getJob().getFlexMillis(),
195                 taskStatus.getJob().getFlexMillis());
196     }
197 
testMassivePeriodClampedOnRead()198     public void testMassivePeriodClampedOnRead() throws Exception {
199         final long ONE_HOUR = 60*60*1000L; // flex
200         final long TWO_HOURS = 2 * ONE_HOUR; // period
201         JobInfo.Builder b = new Builder(8, mComponent)
202                 .setPeriodic(TWO_HOURS, ONE_HOUR)
203                 .setPersisted(true);
204         final long invalidLateRuntimeElapsedMillis =
205                 SystemClock.elapsedRealtime() + (TWO_HOURS * ONE_HOUR) + TWO_HOURS;  // > period+flex
206         final long invalidEarlyRuntimeElapsedMillis =
207                 invalidLateRuntimeElapsedMillis - TWO_HOURS;  // Early is (late - period).
208         final JobStatus js = new JobStatus(b.build(), SOME_UID, "somePackage",
209                 0 /* sourceUserId */, "someTag",
210                 invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis);
211 
212         mTaskStoreUnderTest.add(js);
213         Thread.sleep(IO_WAIT);
214 
215         final JobSet jobStatusSet = new JobSet();
216         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
217         assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size());
218         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
219 
220         // Assert early runtime was clamped to be under now + period. We can do <= here b/c we'll
221         // call SystemClock.elapsedRealtime after doing the disk i/o.
222         final long newNowElapsed = SystemClock.elapsedRealtime();
223         assertTrue("Early runtime wasn't correctly clamped.",
224                 loaded.getEarliestRunTime() <= newNowElapsed + TWO_HOURS);
225         // Assert late runtime was clamped to be now + period + flex.
226         assertTrue("Early runtime wasn't correctly clamped.",
227                 loaded.getEarliestRunTime() <= newNowElapsed + TWO_HOURS + ONE_HOUR);
228     }
229 
testPriorityPersisted()230     public void testPriorityPersisted() throws Exception {
231         JobInfo.Builder b = new Builder(92, mComponent)
232                 .setOverrideDeadline(5000)
233                 .setPriority(42)
234                 .setPersisted(true);
235         final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
236         mTaskStoreUnderTest.add(js);
237         Thread.sleep(IO_WAIT);
238         final JobSet jobStatusSet = new JobSet();
239         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
240         JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
241         assertEquals("Priority not correctly persisted.", 42, loaded.getPriority());
242     }
243 
244     /**
245      * Test that non persisted job is not written to disk.
246      */
testNonPersistedTaskIsNotPersisted()247     public void testNonPersistedTaskIsNotPersisted() throws Exception {
248         JobInfo.Builder b = new Builder(42, mComponent)
249                 .setOverrideDeadline(10000)
250                 .setPersisted(false);
251         JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
252         mTaskStoreUnderTest.add(jsNonPersisted);
253         b = new Builder(43, mComponent)
254                 .setOverrideDeadline(10000)
255                 .setPersisted(true);
256         JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
257         mTaskStoreUnderTest.add(jsPersisted);
258         Thread.sleep(IO_WAIT);
259         final JobSet jobStatusSet = new JobSet();
260         mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet);
261         assertEquals("Job count is incorrect.", 1, jobStatusSet.size());
262         JobStatus jobStatus = jobStatusSet.getAllJobs().iterator().next();
263         assertEquals("Wrong job persisted.", 43, jobStatus.getJobId());
264     }
265 
266     /**
267      * Helper function to throw an error if the provided task and TaskStatus objects are not equal.
268      */
assertTasksEqual(JobInfo first, JobInfo second)269     private void assertTasksEqual(JobInfo first, JobInfo second) {
270         assertEquals("Different task ids.", first.getId(), second.getId());
271         assertEquals("Different components.", first.getService(), second.getService());
272         assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic());
273         assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis());
274         assertEquals("Different inital backoff.", first.getInitialBackoffMillis(),
275                 second.getInitialBackoffMillis());
276         assertEquals("Different backoff policy.", first.getBackoffPolicy(),
277                 second.getBackoffPolicy());
278 
279         assertEquals("Invalid charging constraint.", first.isRequireCharging(),
280                 second.isRequireCharging());
281         assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(),
282                 second.isRequireDeviceIdle());
283         assertEquals("Invalid unmetered constraint.",
284                 first.getNetworkType() == JobInfo.NETWORK_TYPE_UNMETERED,
285                 second.getNetworkType() == JobInfo.NETWORK_TYPE_UNMETERED);
286         assertEquals("Invalid connectivity constraint.",
287                 first.getNetworkType() == JobInfo.NETWORK_TYPE_ANY,
288                 second.getNetworkType() == JobInfo.NETWORK_TYPE_ANY);
289         assertEquals("Invalid deadline constraint.",
290                 first.hasLateConstraint(),
291                 second.hasLateConstraint());
292         assertEquals("Invalid delay constraint.",
293                 first.hasEarlyConstraint(),
294                 second.hasEarlyConstraint());
295         assertEquals("Extras don't match",
296                 first.getExtras().toString(), second.getExtras().toString());
297     }
298 
299     /**
300      * When comparing timestamps before and after DB read/writes (to make sure we're saving/loading
301      * the correct values), there is some latency involved that terrorises a naive assertEquals().
302      * We define a <code>DELTA_MILLIS</code> as a function variable here to make this comparision
303      * more reasonable.
304      */
compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2)305     private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) {
306         final long DELTA_MILLIS = 700L;  // We allow up to 700ms of latency for IO read/writes.
307         assertTrue(error, Math.abs(ts1 - ts2) < DELTA_MILLIS + IO_WAIT);
308     }
309 
310     private static class StubClass {}
311 
312 }
313