1package com.android.server.job;
2
3
4import android.content.ComponentName;
5import android.content.Context;
6import android.app.job.JobInfo;
7import android.app.job.JobInfo.Builder;
8import android.os.PersistableBundle;
9import android.os.SystemClock;
10import android.test.AndroidTestCase;
11import android.test.RenamingDelegatingContext;
12import android.util.Log;
13import android.util.ArraySet;
14
15import com.android.server.job.JobStore.JobSet;
16import com.android.server.job.controllers.JobStatus;
17
18import java.util.Iterator;
19
20/**
21 * Test reading and writing correctly from file.
22 */
23public 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
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
44    public void tearDown() throws Exception {
45        mTaskStoreUnderTest.clear();
46    }
47
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
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
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    }
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
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
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
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     */
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     */
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     */
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