Work on issue #26390151: Add new JobScheduler API...
...for monitoring content providers
Add CTS tests.
Change-Id: Ic88d0666ac384a3b404d2e0fa6c8ed4b4c1a01d5
diff --git a/tests/JobScheduler/src/android/jobscheduler/DummyJobContentProvider.java b/tests/JobScheduler/src/android/jobscheduler/DummyJobContentProvider.java
new file mode 100644
index 0000000..cc8cf21
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/DummyJobContentProvider.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+/**
+ * Stub content provider used for generating content change reports
+ */
+public class DummyJobContentProvider extends ContentProvider {
+ private static final String DATABASE_NAME = "dummy.db";
+ private static final String NAME_VALUE_TABLE = "name_value";
+
+ private DatabaseHelper mDbHelper;
+ private static UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ private static final int MATCH_NAME_VALUE = 1;
+
+ public static final String AUTHORITY = "android.jobscheduler.dummyprovider";
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+
+ public static final String _ID = "_id";
+ public static final String NAME = "name";
+ public static final String VALUE = "value";
+
+ static {
+ sMatcher.addURI(AUTHORITY, null, MATCH_NAME_VALUE);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.content.ContentProvider#onCreate()
+ */
+ @Override
+ public boolean onCreate() {
+ mDbHelper = new DatabaseHelper(getContext());
+ return true;
+ }
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ private static final int DATABASE_VERSION = 1;
+
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ // create an empty name_value table
+ db.execSQL("CREATE TABLE " + NAME_VALUE_TABLE + " (" + _ID + " INTEGER PRIMARY KEY,"
+ + NAME + " TEXT," + VALUE + " TEXT"+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.content.ContentProvider#insert(android.net.Uri,
+ * android.content.ContentValues)
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ String tbName = getTableName(uri);
+ if (tbName == null) {
+ return null;
+ }
+ SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.insert(tbName, VALUE, values);
+ getContext().getContentResolver().notifyChange(uri, null);
+ return uri;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.content.ContentProvider#query(android.net.Uri,
+ * java.lang.String[], java.lang.String, java.lang.String[],
+ * java.lang.String)
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ String tbName = getTableName(uri);
+ if (tbName == null) {
+ return null;
+ }
+ SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ Cursor c = db.query(tbName, projection, selection, selectionArgs, null, null, sortOrder);
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;
+ }
+
+ private String getTableName(Uri uri) {
+ switch (sMatcher.match(uri)) {
+ case MATCH_NAME_VALUE:
+ return NAME_VALUE_TABLE;
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.content.ContentProvider#update(android.net.Uri,
+ * android.content.ContentValues, java.lang.String, java.lang.String[])
+ */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ String tbName = getTableName(uri);
+ if (tbName == null) {
+ return 0;
+ }
+ SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ int count = db.update(tbName, values, selection, selectionArgs);
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.content.ContentProvider#delete(android.net.Uri,
+ * java.lang.String, java.lang.String[])
+ */
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ String tbName = getTableName(uri);
+ if (tbName == null) {
+ return 0;
+ }
+ SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ int count = db.delete(tbName, selection, selectionArgs);
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.content.ContentProvider#getType(android.net.Uri)
+ */
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/TriggerContentJobService.java b/tests/JobScheduler/src/android/jobscheduler/TriggerContentJobService.java
new file mode 100644
index 0000000..d87f1a7
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/TriggerContentJobService.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles callback from the framework {@link android.app.job.JobScheduler}. The behaviour of this
+ * class is configured through the static
+ * {@link TestEnvironment}.
+ */
+@TargetApi(21)
+public class TriggerContentJobService extends JobService {
+ private static final String TAG = "TriggerContentJobService";
+
+ /** Wait this long before timing out the test. */
+ private static final long DEFAULT_TIMEOUT_MILLIS = 30000L; // 30 seconds.
+
+ /** How long to delay before rescheduling the job each time we repeat. */
+ private static final long REPEAT_INTERVAL = 1000L; // 1 second.
+
+ JobInfo mRunningJobInfo;
+ JobParameters mRunningParams;
+
+ final Handler mHandler = new Handler();
+ final Runnable mWorker = new Runnable() {
+ @Override public void run() {
+ scheduleJob(TriggerContentJobService.this, mRunningJobInfo);
+ jobFinished(mRunningParams, false);
+ }
+ };
+
+ public static void scheduleJob(Context context, JobInfo jobInfo) {
+ JobScheduler js = context.getSystemService(JobScheduler.class);
+ js.schedule(jobInfo);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.e(TAG, "Created test service.");
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ Log.i(TAG, "Test job executing: " + params.getJobId());
+
+ int mode = TestEnvironment.getTestEnvironment().getMode();
+ mRunningJobInfo = TestEnvironment.getTestEnvironment().getModeJobInfo();
+ TestEnvironment.getTestEnvironment().setMode(TestEnvironment.MODE_ONESHOT, null);
+ TestEnvironment.getTestEnvironment().notifyExecution(params);
+
+ if (mode == TestEnvironment.MODE_ONE_REPEAT) {
+ mRunningParams = params;
+ mHandler.postDelayed(mWorker, REPEAT_INTERVAL);
+ return true;
+ } else {
+ return false; // No work to do.
+ }
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return false;
+ }
+
+ /**
+ * Configures the expected behaviour for each test. This object is shared across consecutive
+ * tests, so to clear state each test is responsible for calling
+ * {@link TestEnvironment#setUp()}.
+ */
+ public static final class TestEnvironment {
+
+ private static TestEnvironment kTestEnvironment;
+ //public static final int INVALID_JOB_ID = -1;
+
+ private CountDownLatch mLatch;
+ private JobParameters mExecutedJobParameters;
+ private int mMode;
+ private JobInfo mModeJobInfo;
+
+ public static final int MODE_ONESHOT = 0;
+ public static final int MODE_ONE_REPEAT = 1;
+
+ public static TestEnvironment getTestEnvironment() {
+ if (kTestEnvironment == null) {
+ kTestEnvironment = new TestEnvironment();
+ }
+ return kTestEnvironment;
+ }
+
+ public JobParameters getLastJobParameters() {
+ return mExecutedJobParameters;
+ }
+
+ /**
+ * Block the test thread, waiting on the JobScheduler to execute some previously scheduled
+ * job on this service.
+ */
+ public boolean awaitExecution() throws InterruptedException {
+ final boolean executed = mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ return executed;
+ }
+
+ public void setMode(int mode, JobInfo jobInfo) {
+ synchronized (this) {
+ mMode = mode;
+ mModeJobInfo = jobInfo;
+ }
+ }
+
+ public int getMode() {
+ synchronized (this) {
+ return mMode;
+ }
+ }
+
+ public JobInfo getModeJobInfo() {
+ synchronized (this) {
+ return mModeJobInfo;
+ }
+ }
+
+ /**
+ * Block the test thread, expecting to timeout but still listening to ensure that no jobs
+ * land in the interim.
+ * @return True if the latch timed out waiting on an execution.
+ */
+ public boolean awaitTimeout() throws InterruptedException {
+ return !mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ private void notifyExecution(JobParameters params) {
+ Log.d(TAG, "Job executed:" + params.getJobId());
+ mExecutedJobParameters = params;
+ mLatch.countDown();
+ }
+
+ public void setExpectedExecutions(int numExecutions) {
+ // For no executions expected, set count to 1 so we can still block for the timeout.
+ if (numExecutions == 0) {
+ mLatch = new CountDownLatch(1);
+ } else {
+ mLatch = new CountDownLatch(numExecutions);
+ }
+ }
+
+ /** Called in each testCase#setup */
+ public void setUp() {
+ mLatch = null;
+ mExecutedJobParameters = null;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java
index b9a498f..35903d7 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ConstraintTest.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.content.Intent;
import android.jobscheduler.MockJobService;
+import android.jobscheduler.TriggerContentJobService;
import android.test.AndroidTestCase;
/**
@@ -35,14 +36,21 @@
/** Environment that notifies of JobScheduler callbacks. */
static MockJobService.TestEnvironment kTestEnvironment =
MockJobService.TestEnvironment.getTestEnvironment();
+ static TriggerContentJobService.TestEnvironment kTriggerTestEnvironment =
+ TriggerContentJobService.TestEnvironment.getTestEnvironment();
/** Handle for the service which receives the execution callbacks from the JobScheduler. */
static ComponentName kJobServiceComponent;
+ static ComponentName kTriggerContentServiceComponent;
JobScheduler mJobScheduler;
@Override
public void setUp() throws Exception {
+ super.setUp();
kTestEnvironment.setUp();
+ kTriggerTestEnvironment.setUp();
kJobServiceComponent = new ComponentName(getContext(), MockJobService.class);
+ kTriggerContentServiceComponent = new ComponentName(getContext(),
+ TriggerContentJobService.class);
mJobScheduler = (JobScheduler) getContext().getSystemService(Context.JOB_SCHEDULER_SERVICE);
mJobScheduler.cancelAll();
}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/TriggerContentTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/TriggerContentTest.java
new file mode 100644
index 0000000..32959a0
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/TriggerContentTest.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.jobscheduler.cts;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.jobscheduler.DummyJobContentProvider;
+import android.jobscheduler.TriggerContentJobService;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Process;
+import android.provider.MediaStore;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Schedules jobs that look for content URI changes and ensures they are triggered correctly.
+ */
+@TargetApi(23)
+public class TriggerContentTest extends ConstraintTest {
+ public static final int TRIGGER_CONTENT_JOB_ID = ConnectivityConstraintTest.class.hashCode();
+
+ // The root URI of the media provider, to monitor for generic changes to its content.
+ static final Uri MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/");
+
+ // Media URI for all external media content.
+ static final Uri MEDIA_EXTERNAL_URI = Uri.parse("content://" + MediaStore.AUTHORITY
+ + "/external");
+
+ // Path segments for image-specific URIs in the provider.
+ static final List<String> EXTERNAL_PATH_SEGMENTS
+ = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.getPathSegments();
+
+ // The columns we want to retrieve about a particular image.
+ static final String[] PROJECTION = new String[] {
+ MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATA
+ };
+ static final int PROJECTION_ID = 0;
+ static final int PROJECTION_DATA = 1;
+
+ // This is the external storage directory where cameras place pictures.
+ static final String DCIM_DIR = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DCIM).getPath();
+
+ static final String PIC_1_NAME = "TriggerContentTest1_" + Process.myPid();
+ static final String PIC_2_NAME = "TriggerContentTest2_" + Process.myPid();
+
+ File[] mActiveFiles = new File[5];
+ Uri[] mActiveUris = new Uri[5];
+
+ static class MediaScanner implements MediaScannerConnection.OnScanCompletedListener {
+ private static final long DEFAULT_TIMEOUT_MILLIS = 1000L; // 1 second.
+
+ private CountDownLatch mLatch;
+ private String mScannedPath;
+ private Uri mScannedUri;
+
+ public boolean scan(Context context, String file, String mimeType)
+ throws InterruptedException {
+ mLatch = new CountDownLatch(1);
+ MediaScannerConnection.scanFile(context,
+ new String[] { file.toString() }, new String[] { mimeType }, this);
+ return mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ public String getScannedPath() {
+ synchronized (this) {
+ return mScannedPath;
+ }
+ }
+
+ public Uri getScannedUri() {
+ synchronized (this) {
+ return mScannedUri;
+ }
+ }
+
+ @Override public void onScanCompleted(String path, Uri uri) {
+ synchronized (this) {
+ mScannedPath = path;
+ mScannedUri = uri;
+ mLatch.countDown();
+ }
+ }
+ }
+
+ private void cleanupActive(int which) {
+ if (mActiveUris[which] != null) {
+ getContext().getContentResolver().delete(mActiveUris[which], null, null);
+ mActiveUris[which] = null;
+ }
+ if (mActiveFiles[which] != null) {
+ mActiveFiles[which].delete();
+ mActiveFiles[which] = null;
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ for (int i=0; i<mActiveFiles.length; i++) {
+ cleanupActive(i);
+ }
+ }
+
+ private JobInfo makeJobInfo(Uri uri, int flags) {
+ JobInfo.Builder builder = new JobInfo.Builder(TRIGGER_CONTENT_JOB_ID,
+ kTriggerContentServiceComponent);
+ builder.addTriggerContentUri(new JobInfo.TriggerContentUri(uri, flags));
+ // For testing purposes, react quickly.
+ builder.setTriggerContentUpdateDelay(500);
+ builder.setTriggerContentMaxDelay(500);
+ return builder.build();
+ }
+
+ private JobInfo makePhotosJobInfo() {
+ JobInfo.Builder builder = new JobInfo.Builder(TRIGGER_CONTENT_JOB_ID,
+ kTriggerContentServiceComponent);
+ // Look for specific changes to images in the provider.
+ builder.addTriggerContentUri(new JobInfo.TriggerContentUri(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS));
+ // Also look for general reports of changes in the overall provider.
+ builder.addTriggerContentUri(new JobInfo.TriggerContentUri(MEDIA_URI, 0));
+ // For testing purposes, react quickly.
+ builder.setTriggerContentUpdateDelay(500);
+ builder.setTriggerContentMaxDelay(500);
+ return builder.build();
+ }
+
+ public static void copyToFileOrThrow(InputStream inputStream, File destFile)
+ throws IOException {
+ if (destFile.exists()) {
+ destFile.delete();
+ }
+ FileOutputStream out = new FileOutputStream(destFile);
+ try {
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+ } finally {
+ out.flush();
+ try {
+ out.getFD().sync();
+ } catch (IOException e) {
+ }
+ out.close();
+ inputStream.close();
+ }
+ }
+
+ public Uri createAndAddImage(File destFile, InputStream image) throws IOException,
+ InterruptedException {
+ copyToFileOrThrow(image, destFile);
+ MediaScanner scanner = new MediaScanner();
+ boolean success = scanner.scan(getContext(), destFile.toString(), "image/jpeg");
+ if (success) {
+ return scanner.getScannedUri();
+ }
+ return null;
+ }
+
+ public Uri makeActiveFile(int which, File file, InputStream source) throws IOException,
+ InterruptedException {
+ mActiveFiles[which] = file;
+ mActiveUris[which] = createAndAddImage(file, source);
+ return mActiveUris[which];
+ }
+
+ private void assertUriArrayLength(int length, Uri[] uris) {
+ if (uris.length != length) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Expected ");
+ sb.append(length);
+ sb.append(" URI, got ");
+ sb.append(uris.length);
+ if (uris.length > 0) {
+ sb.append(": ");
+ for (int i=0; i<uris.length; i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(uris[i]);
+ }
+ }
+ fail(sb.toString());
+ }
+ }
+
+ private void assertHasUri(Uri wanted, Uri[] uris) {
+ for (int i=0; i<uris.length; i++) {
+ if (wanted.equals(uris[i])) {
+ return;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Don't have uri ");
+ sb.append(wanted);
+ sb.append(" in: ");
+ for (int i=0; i<uris.length; i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(uris[i]);
+ }
+ fail(sb.toString());
+ }
+
+ public void testDescendantsObserver() throws Exception {
+ String base = "content://" + DummyJobContentProvider.AUTHORITY + "/root";
+ Uri uribase = Uri.parse(base);
+ Uri uri1 = Uri.parse(base + "/sub1");
+ Uri uri2 = Uri.parse(base + "/sub2");
+
+ // Start watching.
+ JobInfo triggerJob = makeJobInfo(uribase,
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ kTriggerTestEnvironment.setMode(TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT,
+ triggerJob);
+ mJobScheduler.schedule(triggerJob);
+
+ // Report changes.
+ getContext().getContentResolver().notifyChange(uribase, null, 0);
+ getContext().getContentResolver().notifyChange(uri1, null, 0);
+
+ // Wait and check results
+ boolean executed = kTriggerTestEnvironment.awaitExecution();
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ assertTrue("Timed out waiting for trigger content.", executed);
+ JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
+ Uri[] uris = params.getTriggeredContentUris();
+ assertUriArrayLength(2, uris);
+ assertHasUri(uribase, uris);
+ assertHasUri(uri1, uris);
+ String[] auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
+
+ // Report more changes, this time not letting it see the top-level change
+ getContext().getContentResolver().notifyChange(uribase, null,
+ ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS);
+ getContext().getContentResolver().notifyChange(uri2, null, 0);
+
+ // Wait for the job to wake up and verify it saw the change.
+ executed = kTriggerTestEnvironment.awaitExecution();
+ assertTrue("Timed out waiting for trigger content.", executed);
+ params = kTriggerTestEnvironment.getLastJobParameters();
+ uris = params.getTriggeredContentUris();
+ assertUriArrayLength(1, uris);
+ assertEquals(uri2, uris[0]);
+ auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
+ }
+
+ public void testNonDescendantsObserver() throws Exception {
+ String base = "content://" + DummyJobContentProvider.AUTHORITY + "/root";
+ Uri uribase = Uri.parse(base);
+ Uri uri1 = Uri.parse(base + "/sub1");
+ Uri uri2 = Uri.parse(base + "/sub2");
+
+ // Start watching.
+ JobInfo triggerJob = makeJobInfo(uribase, 0);
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ kTriggerTestEnvironment.setMode(TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT,
+ triggerJob);
+ mJobScheduler.schedule(triggerJob);
+
+ // Report changes.
+ getContext().getContentResolver().notifyChange(uribase, null, 0);
+ getContext().getContentResolver().notifyChange(uri1, null, 0);
+
+ // Wait and check results
+ boolean executed = kTriggerTestEnvironment.awaitExecution();
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ assertTrue("Timed out waiting for trigger content.", executed);
+ JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
+ Uri[] uris = params.getTriggeredContentUris();
+ assertUriArrayLength(1, uris);
+ assertEquals(uribase, uris[0]);
+ String[] auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
+
+ // Report more changes, this time not letting it see the top-level change
+ getContext().getContentResolver().notifyChange(uribase, null,
+ ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS);
+ getContext().getContentResolver().notifyChange(uri2, null, 0);
+
+ // Wait for the job to wake up and verify it saw the change.
+ executed = kTriggerTestEnvironment.awaitExecution();
+ assertTrue("Timed out waiting for trigger content.", executed);
+ params = kTriggerTestEnvironment.getLastJobParameters();
+ uris = params.getTriggeredContentUris();
+ assertUriArrayLength(1, uris);
+ assertEquals(uribase, uris[0]);
+ auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(DummyJobContentProvider.AUTHORITY, auths[0]);
+ }
+
+ public void testPhotoAdded() throws Exception {
+ JobInfo triggerJob = makePhotosJobInfo();
+
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ kTriggerTestEnvironment.setMode(TriggerContentJobService.TestEnvironment.MODE_ONE_REPEAT,
+ triggerJob);
+ mJobScheduler.schedule(triggerJob);
+
+ // Create a file that our job should see.
+ makeActiveFile(0, new File(DCIM_DIR, PIC_1_NAME),
+ getContext().getResources().getAssets().open("violet.jpg"));
+ assertNotNull(mActiveUris[0]);
+
+ // Wait for the job to wake up with the change and verify it.
+ boolean executed = kTriggerTestEnvironment.awaitExecution();
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ assertTrue("Timed out waiting for trigger content.", executed);
+ JobParameters params = kTriggerTestEnvironment.getLastJobParameters();
+ Uri[] uris = params.getTriggeredContentUris();
+ assertUriArrayLength(1, uris);
+ assertEquals(mActiveUris[0], uris[0]);
+ String[] auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(MediaStore.AUTHORITY, auths[0]);
+
+ // While the job is still running, create another file it should see.
+ // (This tests that it will see changes that happen before the next job
+ // is scheduled.)
+ makeActiveFile(1, new File(DCIM_DIR, PIC_2_NAME),
+ getContext().getResources().getAssets().open("violet.jpg"));
+ assertNotNull(mActiveUris[1]);
+
+ // Wait for the job to wake up and verify it saw the change.
+ executed = kTriggerTestEnvironment.awaitExecution();
+ assertTrue("Timed out waiting for trigger content.", executed);
+ params = kTriggerTestEnvironment.getLastJobParameters();
+ uris = params.getTriggeredContentUris();
+ assertUriArrayLength(1, uris);
+ assertEquals(mActiveUris[1], uris[0]);
+ auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(MediaStore.AUTHORITY, auths[0]);
+
+ // Schedule a new job to look at what we see when deleting the files.
+ kTriggerTestEnvironment.setExpectedExecutions(1);
+ kTriggerTestEnvironment.setMode(TriggerContentJobService.TestEnvironment.MODE_ONESHOT,
+ triggerJob);
+ mJobScheduler.schedule(triggerJob);
+
+ // Delete the files. Note that this will result in a general change, not for specific URIs.
+ cleanupActive(0);
+ cleanupActive(1);
+
+ // Wait for the job to wake up and verify it saw the change.
+ executed = kTriggerTestEnvironment.awaitExecution();
+ assertTrue("Timed out waiting for trigger content.", executed);
+ params = kTriggerTestEnvironment.getLastJobParameters();
+ uris = params.getTriggeredContentUris();
+ assertUriArrayLength(1, uris);
+ assertEquals(MEDIA_EXTERNAL_URI, uris[0]);
+ auths = params.getTriggeredContentAuthorities();
+ assertEquals(1, auths.length);
+ assertEquals(MediaStore.AUTHORITY, auths[0]);
+ }
+}