• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.jobscheduler.cts;
18 
19 import android.annotation.TargetApi;
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.jobscheduler.DummyJobContentProvider;
25 import android.jobscheduler.TriggerContentJobService;
26 import android.media.MediaScannerConnection;
27 import android.net.Uri;
28 import android.os.Environment;
29 import android.os.Process;
30 import android.provider.MediaStore;
31 
32 import java.io.File;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.util.List;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 
40 /**
41  * Schedules jobs that look for content URI changes and ensures they are triggered correctly.
42  */
43 @TargetApi(23)
44 public class TriggerContentTest extends BaseJobSchedulerTest {
45     public static final int TRIGGER_CONTENT_JOB_ID = TriggerContentTest.class.hashCode();
46 
47     // The root URI of the media provider, to monitor for generic changes to its content.
48     static final Uri MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/");
49 
50     // Media URI for all external media content.
51     static final Uri MEDIA_EXTERNAL_URI = Uri.parse("content://" + MediaStore.AUTHORITY
52             + "/external");
53 
54     // Path segments for image-specific URIs in the provider.
55     static final List<String> EXTERNAL_PATH_SEGMENTS
56             = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.getPathSegments();
57 
58     // This is the external storage directory where cameras place pictures.
59     static final String DCIM_DIR = Environment.getExternalStoragePublicDirectory(
60             Environment.DIRECTORY_DCIM).getPath();
61 
62     static final String PIC_1_NAME = "TriggerContentTest1_" + Process.myPid();
63     static final String PIC_2_NAME = "TriggerContentTest2_" + Process.myPid();
64 
65     File[] mActiveFiles = new File[5];
66     Uri[] mActiveUris = new Uri[5];
67 
68     static class MediaScanner implements MediaScannerConnection.OnScanCompletedListener {
69         private static final long DEFAULT_TIMEOUT_MILLIS = 1000L; // 1 second.
70 
71         private CountDownLatch mLatch;
72         private String mScannedPath;
73         private Uri mScannedUri;
74 
scan(Context context, String file, String mimeType)75         public boolean scan(Context context, String file, String mimeType)
76                 throws InterruptedException {
77             mLatch = new CountDownLatch(1);
78             MediaScannerConnection.scanFile(context,
79                     new String[] { file.toString() }, new String[] { mimeType }, this);
80             return mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
81         }
82 
getScannedPath()83         public String getScannedPath() {
84             synchronized (this) {
85                 return mScannedPath;
86             }
87         }
88 
getScannedUri()89         public Uri getScannedUri() {
90             synchronized (this) {
91                 return mScannedUri;
92             }
93         }
94 
onScanCompleted(String path, Uri uri)95         @Override public void onScanCompleted(String path, Uri uri) {
96             synchronized (this) {
97                 mScannedPath = path;
98                 mScannedUri = uri;
99                 mLatch.countDown();
100             }
101         }
102     }
103 
cleanupActive(int which)104     private void cleanupActive(int which) {
105         if (mActiveUris[which] != null) {
106             getContext().getContentResolver().delete(mActiveUris[which], null, null);
107             mActiveUris[which] = null;
108         }
109         if (mActiveFiles[which] != null) {
110             mActiveFiles[which].delete();
111             mActiveFiles[which] = null;
112         }
113     }
114 
115     @Override
tearDown()116     public void tearDown() throws Exception {
117         for (int i=0; i<mActiveFiles.length; i++) {
118             cleanupActive(i);
119         }
120         super.tearDown();
121     }
122 
makeJobInfo(Uri uri, int flags)123     private JobInfo makeJobInfo(Uri uri, int flags) {
124         JobInfo.Builder builder = new JobInfo.Builder(TRIGGER_CONTENT_JOB_ID,
125                 kTriggerContentServiceComponent);
126         builder.addTriggerContentUri(new JobInfo.TriggerContentUri(uri, flags));
127         // For testing purposes, react quickly.
128         builder.setTriggerContentUpdateDelay(500);
129         builder.setTriggerContentMaxDelay(500);
130         return builder.build();
131     }
132 
makePhotosJobInfo()133     private JobInfo makePhotosJobInfo() {
134         JobInfo.Builder builder = new JobInfo.Builder(TRIGGER_CONTENT_JOB_ID,
135                 kTriggerContentServiceComponent);
136         // Look for general reports of changes in the overall provider.
137         builder.addTriggerContentUri(new JobInfo.TriggerContentUri(
138                 MEDIA_URI,
139                 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
140         // For testing purposes, react quickly.
141         builder.setTriggerContentUpdateDelay(500);
142         builder.setTriggerContentMaxDelay(500);
143         return builder.build();
144     }
145 
copyToFileOrThrow(InputStream inputStream, File destFile)146     public static void copyToFileOrThrow(InputStream inputStream, File destFile)
147             throws IOException {
148         if (destFile.exists()) {
149             destFile.delete();
150         }
151         destFile.getParentFile().mkdirs();
152         FileOutputStream out = new FileOutputStream(destFile);
153         try {
154             byte[] buffer = new byte[4096];
155             int bytesRead;
156             while ((bytesRead = inputStream.read(buffer)) >= 0) {
157                 out.write(buffer, 0, bytesRead);
158             }
159         } finally {
160             out.flush();
161             try {
162                 out.getFD().sync();
163             } catch (IOException e) {
164             }
165             out.close();
166             inputStream.close();
167         }
168     }
169 
createAndAddImage(File destFile, InputStream image)170     public Uri createAndAddImage(File destFile, InputStream image) throws IOException,
171             InterruptedException {
172         copyToFileOrThrow(image, destFile);
173         MediaScanner scanner = new MediaScanner();
174         boolean success = scanner.scan(getContext(), destFile.toString(), "image/jpeg");
175         if (success) {
176             return scanner.getScannedUri();
177         }
178         return null;
179     }
180 
makeActiveFile(int which, File file, InputStream source)181     public Uri makeActiveFile(int which, File file, InputStream source) throws IOException,
182                 InterruptedException {
183         mActiveFiles[which] = file;
184         mActiveUris[which] = createAndAddImage(file, source);
185         return mActiveUris[which];
186     }
187 
assertUriArrayLength(int length, Uri[] uris)188     private static void assertUriArrayLength(int length, Uri[] uris) {
189         if (uris.length != length) {
190             StringBuilder sb = new StringBuilder();
191             sb.append("Expected ");
192             sb.append(length);
193             sb.append(" URI, got ");
194             sb.append(uris.length);
195             if (uris.length > 0) {
196                 sb.append(": ");
197                 for (int i=0; i<uris.length; i++) {
198                     if (i > 0) {
199                         sb.append(", ");
200                     }
201                     sb.append(uris[i]);
202                 }
203             }
204             fail(sb.toString());
205         }
206     }
207 
assertHasUri(Uri wanted, Uri[] uris)208     private static void assertHasUri(Uri wanted, Uri[] uris) {
209         for (int i=0; i<uris.length; i++) {
210             if (wanted.equals(uris[i])) {
211                 return;
212             }
213         }
214 
215         StringBuilder sb = new StringBuilder();
216         sb.append("Don't have uri ");
217         sb.append(wanted);
218         sb.append(" in: ");
219         for (int i=0; i<uris.length; i++) {
220             if (i > 0) {
221                 sb.append(", ");
222             }
223             sb.append(uris[i]);
224         }
225         fail(sb.toString());
226     }
227 
assertUriDecendant(Uri expected, Uri actual)228     private static void assertUriDecendant(Uri expected, Uri actual) {
229         assertEquals(expected.getScheme(), expected.getScheme());
230         assertEquals(expected.getAuthority(), expected.getAuthority());
231 
232         final List<String> expectedPath = expected.getPathSegments();
233         final List<String> actualPath = actual.getPathSegments();
234         for (int i = 0; i < expectedPath.size(); i++) {
235             assertEquals(expectedPath.get(i), actualPath.get(i));
236         }
237     }
238 
testDescendantsObserver()239     public void testDescendantsObserver() throws Exception {
240         String base = "content://" + DummyJobContentProvider.AUTHORITY + "/root";
241         Uri uribase = Uri.parse(base);
242         Uri uri1 = Uri.parse(base + "/sub1");
243         Uri uri2 = Uri.parse(base + "/sub2");
244 
245         // Start watching.
246         JobInfo triggerJob = makeJobInfo(uribase,
247                 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
248         kTriggerTestEnvironment.setExpectedExecutions(1);
249         kTriggerTestEnvironment.setMode(
250                 TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT_RESCHEDULE, triggerJob);
251         mJobScheduler.schedule(triggerJob);
252 
253         // Report changes.
254         getContext().getContentResolver().notifyChange(uribase, null, 0);
255         getContext().getContentResolver().notifyChange(uri1, null, 0);
256 
257         // Wait and check results
258         boolean executed = kTriggerTestEnvironment.awaitExecution();
259         kTriggerTestEnvironment.setExpectedExecutions(1);
260         assertTrue("Timed out waiting for trigger content.", executed);
261         JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
262         Uri[] uris = params.getTriggeredContentUris();
263         assertUriArrayLength(2, uris);
264         assertHasUri(uribase, uris);
265         assertHasUri(uri1, uris);
266         String[] auths = params.getTriggeredContentAuthorities();
267         assertEquals(1, auths.length);
268         assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
269 
270         // Report more changes, this time not letting it see the top-level change
271         getContext().getContentResolver().notifyChange(uribase, null,
272                 ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS);
273         getContext().getContentResolver().notifyChange(uri2, null, 0);
274 
275         // Wait for the job to wake up and verify it saw the change.
276         executed = kTriggerTestEnvironment.awaitExecution();
277         assertTrue("Timed out waiting for trigger content.", executed);
278         params = kTriggerTestEnvironment.getLastJobParameters();
279         uris = params.getTriggeredContentUris();
280         assertUriArrayLength(1, uris);
281         assertEquals(uri2, uris[0]);
282         auths = params.getTriggeredContentAuthorities();
283         assertEquals(1, auths.length);
284         assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
285     }
286 
testNonDescendantsObserver()287     public void testNonDescendantsObserver() throws Exception {
288         String base = "content://" + DummyJobContentProvider.AUTHORITY + "/root";
289         Uri uribase = Uri.parse(base);
290         Uri uri1 = Uri.parse(base + "/sub1");
291         Uri uri2 = Uri.parse(base + "/sub2");
292 
293         // Start watching.
294         JobInfo triggerJob = makeJobInfo(uribase, 0);
295         kTriggerTestEnvironment.setExpectedExecutions(1);
296         kTriggerTestEnvironment.setMode(
297                 TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT_RESCHEDULE, triggerJob);
298         mJobScheduler.schedule(triggerJob);
299 
300         // Report changes.
301         getContext().getContentResolver().notifyChange(uribase, null, 0);
302         getContext().getContentResolver().notifyChange(uri1, null, 0);
303 
304         // Wait and check results
305         boolean executed = kTriggerTestEnvironment.awaitExecution();
306         kTriggerTestEnvironment.setExpectedExecutions(1);
307         assertTrue("Timed out waiting for trigger content.", executed);
308         JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
309         Uri[] uris = params.getTriggeredContentUris();
310         assertUriArrayLength(1, uris);
311         assertEquals(uribase, uris[0]);
312         String[] auths = params.getTriggeredContentAuthorities();
313         assertEquals(1, auths.length);
314         assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
315 
316         // Report more changes, this time not letting it see the top-level change
317         getContext().getContentResolver().notifyChange(uribase, null,
318                 ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS);
319         getContext().getContentResolver().notifyChange(uri2, null, 0);
320 
321         // Wait for the job to wake up and verify it saw the change.
322         executed = kTriggerTestEnvironment.awaitExecution();
323         assertTrue("Timed out waiting for trigger content.", executed);
324         params = kTriggerTestEnvironment.getLastJobParameters();
325         uris = params.getTriggeredContentUris();
326         assertUriArrayLength(1, uris);
327         assertEquals(uribase, uris[0]);
328         auths = params.getTriggeredContentAuthorities();
329         assertEquals(1, auths.length);
330         assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
331     }
332 
testPhotoAdded_Reschedule()333     public void testPhotoAdded_Reschedule() throws Exception {
334         JobInfo triggerJob = makePhotosJobInfo();
335 
336         kTriggerTestEnvironment.setExpectedExecutions(1);
337         kTriggerTestEnvironment.setMode(
338                 TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT_RESCHEDULE, triggerJob);
339         mJobScheduler.schedule(triggerJob);
340 
341         // Create a file that our job should see.
342         makeActiveFile(0, new File(DCIM_DIR, PIC_1_NAME),
343                 getContext().getResources().getAssets().open("violet.jpg"));
344         assertNotNull(mActiveUris[0]);
345 
346         // Wait for the job to wake up with the change and verify it.
347         boolean executed = kTriggerTestEnvironment.awaitExecution();
348         kTriggerTestEnvironment.setExpectedExecutions(1);
349         assertTrue("Timed out waiting for trigger content.", executed);
350         JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
351         Uri[] uris = params.getTriggeredContentUris();
352         for (Uri uri : uris) {
353             assertUriDecendant(MEDIA_URI, uri);
354         }
355         String[] auths = params.getTriggeredContentAuthorities();
356         assertEquals(1, auths.length);
357         assertEquals(MediaStore.AUTHORITY, auths[0]);
358 
359         // While the job is still running, create another file it should see.
360         // (This tests that it will see changes that happen before the next job
361         // is scheduled.)
362         makeActiveFile(1, new File(DCIM_DIR, PIC_2_NAME),
363                 getContext().getResources().getAssets().open("violet.jpg"));
364         assertNotNull(mActiveUris[1]);
365 
366         // Wait for the job to wake up and verify it saw the change.
367         executed = kTriggerTestEnvironment.awaitExecution();
368         assertTrue("Timed out waiting for trigger content.", executed);
369         params = kTriggerTestEnvironment.getLastJobParameters();
370         uris = params.getTriggeredContentUris();
371         for (Uri uri : uris) {
372             assertUriDecendant(MEDIA_URI, uri);
373         }
374         auths = params.getTriggeredContentAuthorities();
375         assertEquals(1, auths.length);
376         assertEquals(MediaStore.AUTHORITY, auths[0]);
377 
378         // Schedule a new job to look at what we see when deleting the files.
379         kTriggerTestEnvironment.setExpectedExecutions(1);
380         kTriggerTestEnvironment.setMode(TriggerContentJobService.TestEnvironment.MODE_ONESHOT,
381                 triggerJob);
382         mJobScheduler.schedule(triggerJob);
383 
384         // Delete the files.  Note that this will result in a general change, not for specific URIs.
385         cleanupActive(0);
386         cleanupActive(1);
387 
388         // Wait for the job to wake up and verify it saw the change.
389         executed = kTriggerTestEnvironment.awaitExecution();
390         assertTrue("Timed out waiting for trigger content.", executed);
391         params = kTriggerTestEnvironment.getLastJobParameters();
392         uris = params.getTriggeredContentUris();
393         for (Uri uri : uris) {
394             assertUriDecendant(MEDIA_URI, uri);
395         }
396         auths = params.getTriggeredContentAuthorities();
397         assertEquals(1, auths.length);
398         assertEquals(MediaStore.AUTHORITY, auths[0]);
399     }
400 
401     // Doesn't work.  Should it?
xxxtestPhotoAdded_FinishTrue()402     public void xxxtestPhotoAdded_FinishTrue() throws Exception {
403         JobInfo triggerJob = makePhotosJobInfo();
404 
405         kTriggerTestEnvironment.setExpectedExecutions(1);
406         kTriggerTestEnvironment.setMode(
407                 TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT_FINISH_TRUE, triggerJob);
408         mJobScheduler.schedule(triggerJob);
409 
410         // Create a file that our job should see.
411         makeActiveFile(0, new File(DCIM_DIR, PIC_1_NAME),
412                 getContext().getResources().getAssets().open("violet.jpg"));
413         assertNotNull(mActiveUris[0]);
414 
415         // Wait for the job to wake up with the change and verify it.
416         boolean executed = kTriggerTestEnvironment.awaitExecution();
417         kTriggerTestEnvironment.setExpectedExecutions(1);
418         assertTrue("Timed out waiting for trigger content.", executed);
419         JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
420         Uri[] uris = params.getTriggeredContentUris();
421         assertUriArrayLength(1, uris);
422         assertEquals(mActiveUris[0], uris[0]);
423         String[] auths = params.getTriggeredContentAuthorities();
424         assertEquals(1, auths.length);
425         assertEquals(MediaStore.AUTHORITY, auths[0]);
426 
427         // While the job is still running, create another file it should see.
428         // (This tests that it will see changes that happen before the next job
429         // is scheduled.)
430         makeActiveFile(1, new File(DCIM_DIR, PIC_2_NAME),
431                 getContext().getResources().getAssets().open("violet.jpg"));
432         assertNotNull(mActiveUris[1]);
433 
434         // Wait for the job to wake up and verify it saw the change.
435         executed = kTriggerTestEnvironment.awaitExecution();
436         assertTrue("Timed out waiting for trigger content.", executed);
437         params = kTriggerTestEnvironment.getLastJobParameters();
438         uris = params.getTriggeredContentUris();
439         assertUriArrayLength(1, uris);
440         assertEquals(mActiveUris[1], uris[0]);
441         auths = params.getTriggeredContentAuthorities();
442         assertEquals(1, auths.length);
443         assertEquals(MediaStore.AUTHORITY, auths[0]);
444 
445         // Schedule a new job to look at what we see when deleting the files.
446         kTriggerTestEnvironment.setExpectedExecutions(1);
447         kTriggerTestEnvironment.setMode(TriggerContentJobService.TestEnvironment.MODE_ONESHOT,
448                 triggerJob);
449         mJobScheduler.schedule(triggerJob);
450 
451         // Delete the files.  Note that this will result in a general change, not for specific URIs.
452         cleanupActive(0);
453         cleanupActive(1);
454 
455         // Wait for the job to wake up and verify it saw the change.
456         executed = kTriggerTestEnvironment.awaitExecution();
457         assertTrue("Timed out waiting for trigger content.", executed);
458         params = kTriggerTestEnvironment.getLastJobParameters();
459         uris = params.getTriggeredContentUris();
460         assertUriArrayLength(1, uris);
461         assertEquals(MEDIA_EXTERNAL_URI, uris[0]);
462         auths = params.getTriggeredContentAuthorities();
463         assertEquals(1, auths.length);
464         assertEquals(MediaStore.AUTHORITY, auths[0]);
465     }
466 }
467