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