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