Add Content Uri Triggers to SystemJobSchedulerConverter
Excluded jobs with Content Uri Triggers from ForegroundProcessor.

Test: Added test to SystemJobInfoConverterTest.
      Updated test in WorkManagerTest.
Change-Id: I256bacd53f3d36ca013ef60f7ff4545ed336589c
diff --git a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/WorkManagerTest.java b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/WorkManagerTest.java
index 20b03ed..bd9b7ee 100644
--- a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/WorkManagerTest.java
+++ b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/WorkManagerTest.java
@@ -31,6 +31,7 @@
 import android.arch.persistence.db.SupportSQLiteDatabase;
 import android.arch.persistence.db.SupportSQLiteOpenHelper;
 import android.net.Uri;
+import android.provider.MediaStore;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -129,8 +130,8 @@
 
     @Test
     public void testEnqueue_insertWorkConstraints() throws InterruptedException {
-        Uri testUri1 = Uri.parse("TEST_URI_1");
-        Uri testUri2 = Uri.parse("TEST_URI_2");
+        Uri testUri1 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+        Uri testUri2 = MediaStore.Images.Media.INTERNAL_CONTENT_URI;
 
         Work work0 = new Work.Builder(TestWorker.class)
                 .withConstraints(
diff --git a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverterTest.java b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverterTest.java
index 6f99143..fccbecf 100644
--- a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverterTest.java
+++ b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverterTest.java
@@ -20,6 +20,7 @@
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.arrayContaining;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -29,6 +30,7 @@
 import android.arch.background.workmanager.model.Constraints;
 import android.arch.background.workmanager.model.WorkSpec;
 import android.arch.background.workmanager.worker.TestWorker;
+import android.net.Uri;
 import android.os.Build;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SdkSuppress;
@@ -140,6 +142,21 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 24)
+    public void testConvert_requireContentUriTrigger() {
+        final Uri expectedUri = Uri.parse("TEST_URI");
+        final JobInfo.TriggerContentUri expectedTriggerContentUri =
+                new JobInfo.TriggerContentUri(
+                        expectedUri, JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
+        WorkSpec workSpec = getWorkSpec(TestWorker.class, new Constraints.Builder()
+                .addContentUriTrigger(expectedUri, true).build());
+        JobInfo jobInfo = mConverter.convert(workSpec);
+
+        JobInfo.TriggerContentUri[] triggerContentUris = jobInfo.getTriggerContentUris();
+        assertThat(triggerContentUris, is(arrayContaining(expectedTriggerContentUri)));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 24)
     public void testConvert_requireDeviceIdle() {
         final boolean expectedRequireDeviceIdle = true;
         WorkSpec workSpec = getWorkSpec(TestWorker.class, new Constraints.Builder()
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/model/Constraints.java b/background/workmanager/src/main/java/android/arch/background/workmanager/model/Constraints.java
index e876538..b0063d8 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/model/Constraints.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/model/Constraints.java
@@ -132,6 +132,13 @@
         return mContentUriTriggers;
     }
 
+    /**
+     * @return {@code true} if {@link ContentUriTriggers} is not empty
+     */
+    public boolean hasContentUriTriggers() {
+        return mContentUriTriggers.size() > 0;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/model/ContentUriTriggers.java b/background/workmanager/src/main/java/android/arch/background/workmanager/model/ContentUriTriggers.java
index 5afe27f..9553cc2 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/model/ContentUriTriggers.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/model/ContentUriTriggers.java
@@ -30,11 +30,11 @@
 import java.util.Set;
 
 /**
- * Stores a set of {@link ContentUriTrigger}s
+ * Stores a set of {@link Trigger}s
  */
 
-public class ContentUriTriggers implements Iterable<ContentUriTriggers.ContentUriTrigger> {
-    private final Set<ContentUriTrigger> mTriggers = new HashSet<>();
+public class ContentUriTriggers implements Iterable<ContentUriTriggers.Trigger> {
+    private final Set<Trigger> mTriggers = new HashSet<>();
 
     /**
      * Add a Content {@link Uri} to observe
@@ -43,38 +43,42 @@
      *                              {@link WorkSpec} to run
      */
     public void add(Uri uri, boolean triggerForDescendants) {
-        ContentUriTrigger trigger = new ContentUriTrigger(uri, triggerForDescendants);
+        Trigger trigger = new Trigger(uri, triggerForDescendants);
         mTriggers.add(trigger);
     }
 
     @NonNull
     @Override
-    public Iterator<ContentUriTrigger> iterator() {
+    public Iterator<Trigger> iterator() {
         return mTriggers.iterator();
     }
 
     /**
-     * @return number of {@link ContentUriTrigger} objects
+     * @return number of {@link Trigger} objects
      */
     public int size() {
         return mTriggers.size();
     }
 
     /**
-     * Converts a list of {@link ContentUriTrigger}s to byte array representation
-     * @param triggers the list of {@link ContentUriTrigger}s to convert
+     * Converts a list of {@link Trigger}s to byte array representation
+     * @param triggers the list of {@link Trigger}s to convert
      * @return corresponding byte array representation
      */
     @TypeConverter
     public static byte[] toByteArray(ContentUriTriggers triggers) {
+        if (triggers.size() == 0) {
+            // Return null for no triggers. Needed for SQL query check in ForegroundProcessor
+            return null;
+        }
         ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         ObjectOutputStream objectOutputStream = null;
         try {
             objectOutputStream = new ObjectOutputStream(outputStream);
             objectOutputStream.writeInt(triggers.size());
-            for (ContentUriTrigger trigger : triggers) {
+            for (Trigger trigger : triggers) {
                 objectOutputStream.writeUTF(trigger.getUri().toString());
-                objectOutputStream.writeBoolean(trigger.isTriggerForDescendants());
+                objectOutputStream.writeBoolean(trigger.shouldTriggerForDescendants());
             }
         } catch (IOException e) {
             e.printStackTrace();
@@ -96,13 +100,17 @@
     }
 
     /**
-     * Converts a byte array to list of {@link ContentUriTrigger}s
+     * Converts a byte array to list of {@link Trigger}s
      * @param bytes byte array representation to convert
-     * @return list of {@link ContentUriTrigger}s
+     * @return list of {@link Trigger}s
      */
     @TypeConverter
     public static ContentUriTriggers fromByteArray(byte[] bytes) {
         ContentUriTriggers triggers = new ContentUriTriggers();
+        if (bytes == null) {
+            // bytes will be null if there are no Content Uri Triggers
+            return triggers;
+        }
         ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
         ObjectInputStream objectInputStream = null;
         try {
@@ -150,13 +158,13 @@
      * Defines a content {@link Uri} trigger for a {@link WorkSpec}
      */
 
-    public static class ContentUriTrigger {
+    public static class Trigger {
         @NonNull
         private final Uri mUri;
         private final boolean mTriggerForDescendants;
 
-        public ContentUriTrigger(@NonNull Uri uri,
-                                 boolean triggerForDescendants) {
+        public Trigger(@NonNull Uri uri,
+                       boolean triggerForDescendants) {
             mUri = uri;
             mTriggerForDescendants = triggerForDescendants;
         }
@@ -166,7 +174,10 @@
             return mUri;
         }
 
-        public boolean isTriggerForDescendants() {
+        /**
+         * @return {@code true} if trigger applies to descendants of {@link Uri} also
+         */
+        public boolean shouldTriggerForDescendants() {
             return mTriggerForDescendants;
         }
 
@@ -175,7 +186,7 @@
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
 
-            ContentUriTrigger trigger = (ContentUriTrigger) o;
+            Trigger trigger = (Trigger) o;
 
             return mTriggerForDescendants == trigger.mTriggerForDescendants
                     && mUri.equals(trigger.mUri);
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/model/WorkSpecDao.java b/background/workmanager/src/main/java/android/arch/background/workmanager/model/WorkSpecDao.java
index 72e7522..8761e71 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/model/WorkSpecDao.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/model/WorkSpecDao.java
@@ -124,7 +124,8 @@
      */
     @Query("SELECT * FROM workspec WHERE status=" + STATUS_ENQUEUED + " AND "
             + " requires_charging=0 AND requires_device_idle=0 AND requires_battery_not_low=0 AND "
-            + " requires_storage_not_low=0 AND required_network_type=0 AND interval_duration=0")
+            + " requires_storage_not_low=0 AND required_network_type=0 AND interval_duration=0 AND"
+            + " content_uri_triggers IS NULL")
     LiveData<List<WorkSpec>> getForegroundEligibleWorkSpecs();
 
     /**
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverter.java b/background/workmanager/src/main/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverter.java
index 037768b..0b5695d 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverter.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/systemjob/SystemJobInfoConverter.java
@@ -21,6 +21,7 @@
 import android.app.job.JobInfo;
 import android.arch.background.workmanager.Work;
 import android.arch.background.workmanager.model.Constraints;
+import android.arch.background.workmanager.model.ContentUriTriggers;
 import android.arch.background.workmanager.model.WorkSpec;
 import android.content.ComponentName;
 import android.content.Context;
@@ -75,7 +76,6 @@
         PersistableBundle extras = new PersistableBundle();
         extras.putString(EXTRA_WORK_SPEC_ID, workSpec.getId());
         JobInfo.Builder builder = new JobInfo.Builder(jobId, mWorkServiceComponent)
-                .setPersisted(true)
                 .setRequiredNetworkType(jobInfoNetworkType)
                 .setExtras(extras);
 
@@ -94,6 +94,15 @@
             builder.setMinimumLatency(workSpec.getInitialDelay());
         }
 
+        if (Build.VERSION.SDK_INT >= 24 && constraints.hasContentUriTriggers()) {
+            for (ContentUriTriggers.Trigger trigger : constraints.getContentUriTriggers()) {
+                builder.addTriggerContentUri(convertContentUriTrigger(trigger));
+            }
+        } else {
+            // Jobs with Content Uri Triggers cannot be persisted
+            builder.setPersisted(true);
+        }
+
         // TODO(janclarin): Support requiresCharging/requiresDeviceIdle for versions older than 24.
         if (Build.VERSION.SDK_INT >= 24) {
             builder.setRequiresCharging(constraints.requiresCharging());
@@ -108,6 +117,14 @@
         return builder.build();
     }
 
+    @RequiresApi(24)
+    private static JobInfo.TriggerContentUri convertContentUriTrigger(
+            ContentUriTriggers.Trigger trigger) {
+        int flag = trigger.shouldTriggerForDescendants()
+                ? JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS : 0;
+        return new JobInfo.TriggerContentUri(trigger.getUri(), flag);
+    }
+
     /**
      * Converts {@link Constraints.NetworkType} into {@link JobInfo}'s network values.
      *