Start a uniquely tagged work sequence.

This allows you to have "singleton work chains",
meaning you can enforce that only one of a designated
chain of work executes at a time.

Right now, there is an open issue that if you call
this multiple times, getting work by that tag will
return you all the workers; I need to think about how
to address this.  Perhaps this processing needs to
happen immediately, but that might mean changing
the way WM.enqueue works and turning it into
something like:
WM.enqueue().then().then()...start();

(How do we feel about that?  It may actually be a
better way to go because you can create a chain
without starting pre-emptive execution.)

Test: added and ran tests.

Change-Id: I0d1c74a75ca923824daef649c641a5ca5df9a640
diff --git a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkManagerImplTest.java b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkManagerImplTest.java
index 0a60abe..405b095 100644
--- a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkManagerImplTest.java
+++ b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkManagerImplTest.java
@@ -29,6 +29,7 @@
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.emptyCollectionOf;
 import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.isIn;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -40,7 +41,9 @@
 import android.arch.background.workmanager.PeriodicWork;
 import android.arch.background.workmanager.TestLifecycleOwner;
 import android.arch.background.workmanager.Work;
+import android.arch.background.workmanager.WorkManager;
 import android.arch.background.workmanager.WorkManagerTest;
+import android.arch.background.workmanager.executors.SynchronousExecutorService;
 import android.arch.background.workmanager.impl.model.Dependency;
 import android.arch.background.workmanager.impl.model.DependencyDao;
 import android.arch.background.workmanager.impl.model.WorkSpec;
@@ -86,16 +89,6 @@
 
     @Before
     public void setUp() {
-        Context context = InstrumentationRegistry.getTargetContext();
-        WorkManagerConfiguration configuration = new WorkManagerConfiguration(
-                context,
-                true,
-                WorkManagerConfiguration.createForegroundExecutorService(),
-                WorkManagerConfiguration.createBackgroundExecutorService(),
-                ProcessLifecycleOwner.get());
-        mWorkManagerImpl = new WorkManagerImpl(context, configuration);
-        mDatabase = mWorkManagerImpl.getWorkDatabase();
-
         ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
             @Override
             public void executeOnDiskIO(@NonNull Runnable runnable) {
@@ -112,6 +105,17 @@
                 return true;
             }
         });
+
+        Context context = InstrumentationRegistry.getTargetContext();
+        WorkManagerConfiguration configuration = new WorkManagerConfiguration(
+                context,
+                true,
+                new SynchronousExecutorService(),
+                new SynchronousExecutorService(),
+                ProcessLifecycleOwner.get());
+        mWorkManagerImpl = new WorkManagerImpl(context, configuration);
+        mDatabase = mWorkManagerImpl.getWorkDatabase();
+        WorkManagerImpl.setInstance(mWorkManagerImpl);
     }
 
     @After
@@ -339,6 +343,69 @@
 
     @Test
     @SmallTest
+    public void testUniqueTagSequence_setsUniqueTag() {
+        final String testTag = "mytag";
+
+        Work work = Work.newBuilder(TestWorker.class).build();
+        mWorkManagerImpl.startSequenceWithUniqueTag(testTag, WorkManager.REPLACE_EXISTING_WORK)
+                .then(work);
+
+        List<String> workSpecIds = mDatabase.workTagDao().getWorkSpecsWithTag(testTag);
+        assertThat(work.getId(), isIn(workSpecIds));
+    }
+
+    @Test
+    @SmallTest
+    public void testUniqueTagSequence_deletesOldTagsOnReplace() {
+        final String testTag = "mytag";
+
+        Work originalWork = Work.newBuilder(TestWorker.class).addTag(testTag).build();
+        insertWorkSpecAndTags(originalWork);
+
+        Work replacementWork1 = Work.newBuilder(TestWorker.class).build();
+        Work replacementWork2 = Work.newBuilder(TestWorker.class).build();
+        mWorkManagerImpl
+                .startSequenceWithUniqueTag(
+                        testTag, WorkManager.REPLACE_EXISTING_WORK, replacementWork1)
+                .then(replacementWork2);
+
+        List<String> workSpecIds = mDatabase.workTagDao().getWorkSpecsWithTag(testTag);
+        assertThat(
+                workSpecIds,
+                containsInAnyOrder(replacementWork1.getId(), replacementWork2.getId()));
+
+        WorkSpecDao workSpecDao = mDatabase.workSpecDao();
+        assertThat(workSpecDao.getWorkSpec(originalWork.getId()), is(nullValue()));
+        assertThat(workSpecDao.getWorkSpec(replacementWork1.getId()), is(not(nullValue())));
+        assertThat(workSpecDao.getWorkSpec(replacementWork2.getId()), is(not(nullValue())));
+    }
+
+    @Test
+    @SmallTest
+    public void testUniqueTagSequence_keepsExistingWorkOnKeep() {
+        final String testTag = "mytag";
+
+        Work originalWork = Work.newBuilder(TestWorker.class).addTag(testTag).build();
+        insertWorkSpecAndTags(originalWork);
+
+        Work replacementWork1 = Work.newBuilder(TestWorker.class).build();
+        Work replacementWork2 = Work.newBuilder(TestWorker.class).build();
+        mWorkManagerImpl
+                .startSequenceWithUniqueTag(
+                        testTag, WorkManager.KEEP_EXISTING_WORK, replacementWork1)
+                .then(replacementWork2);
+
+        List<String> workSpecIds = mDatabase.workTagDao().getWorkSpecsWithTag(testTag);
+        assertThat(workSpecIds, containsInAnyOrder(originalWork.getId()));
+
+        WorkSpecDao workSpecDao = mDatabase.workSpecDao();
+        assertThat(workSpecDao.getWorkSpec(originalWork.getId()), is(not(nullValue())));
+        assertThat(workSpecDao.getWorkSpec(replacementWork1.getId()), is(nullValue()));
+        assertThat(workSpecDao.getWorkSpec(replacementWork2.getId()), is(nullValue()));
+    }
+
+    @Test
+    @SmallTest
     @SuppressWarnings("unchecked")
     public void testGetStatuses() {
         Work work0 = Work.newBuilder(TestWorker.class).build();
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/BaseWork.java b/background/workmanager/src/main/java/android/arch/background/workmanager/BaseWork.java
index b9ea188..9d29d55 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/BaseWork.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/BaseWork.java
@@ -138,7 +138,7 @@
          * @param tag A tag for identifying the work in queries.
          * @return The current {@link Builder}.
          */
-        B addTag(String tag);
+        B addTag(@NonNull String tag);
 
         /**
          * Builds this work object.
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/WorkManager.java b/background/workmanager/src/main/java/android/arch/background/workmanager/WorkManager.java
index 229a1ee..f5d9a9f 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/WorkManager.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/WorkManager.java
@@ -16,10 +16,15 @@
 
 package android.arch.background.workmanager;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.arch.background.workmanager.impl.WorkManagerImpl;
 import android.arch.lifecycle.LiveData;
+import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 
+import java.lang.annotation.Retention;
+
 /**
  * WorkManager is a class used to enqueue persisted work that is guaranteed to run after its
  * constraints are met.
@@ -72,6 +77,30 @@
     public abstract WorkContinuation enqueue(@NonNull Class<? extends Worker>... workerClasses);
 
     /**
+     * This method allows you to create unique chains of work for situations where you only want one
+     * chain to be active at a given time.  For example, you may only want one sync operation to be
+     * active.  If there is one pending, you can choose to let it run or replace it with your new
+     * work.
+     *
+     * All work in this chain will be automatically tagged with {@code tag} if it isn't already.
+     *
+     * If this method determines that new work should be enqueued and run, all records of previous
+     * work with {@code tag} will be pruned.  If this method determines that new work should NOT be
+     * run, then the entire chain will be considered a no-op.
+     *
+     * @param tag A tag which should uniquely label all the work in this chain
+     * @param existingWorkPolicy One of {@code REPLACE_EXISTING_WORK} or {@code KEEP_EXISTING_WORK}.
+     *                           {@code REPLACE_EXISTING_WORK} ensures that if there is pending work
+     *                           labelled with {@code tag}, it will be cancelled and the new work
+     *                           will run.  {@code KEEP_EXISTING_WORK} will run the new sequence of
+     *                           work only if there is no pending work labelled with {@code tag}.
+     * @param work One or more {@link Work} to enqueue
+     * @return A {@link WorkContinuation} that allows further chaining
+     */
+    public abstract WorkContinuation startSequenceWithUniqueTag(
+            @NonNull String tag, @ExistingWorkPolicy int existingWorkPolicy, @NonNull Work... work);
+
+    /**
      * Enqueues one or more periodic work items for background processing.
      *
      * @param periodicWork One or more {@link PeriodicWork} to enqueue
@@ -101,5 +130,12 @@
      * outputs stored in the database.
      */
     public abstract void pruneDatabase();
-}
 
+    @Retention(SOURCE)
+    @IntDef({REPLACE_EXISTING_WORK, KEEP_EXISTING_WORK})
+    public @interface ExistingWorkPolicy {
+    }
+
+    public static final int REPLACE_EXISTING_WORK = 0;
+    public static final int KEEP_EXISTING_WORK = 1;
+}
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/PeriodicWorkImpl.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/PeriodicWorkImpl.java
index a329390..8be2b21 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/PeriodicWorkImpl.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/PeriodicWorkImpl.java
@@ -122,7 +122,7 @@
         }
 
         @Override
-        public PeriodicWork.Builder addTag(String tag) {
+        public PeriodicWork.Builder addTag(@NonNull String tag) {
             mTags.add(tag);
             return this;
         }
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkContinuationImpl.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkContinuationImpl.java
index c972e47..a0dc545 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkContinuationImpl.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkContinuationImpl.java
@@ -18,10 +18,12 @@
 
 import android.arch.background.workmanager.Work;
 import android.arch.background.workmanager.WorkContinuation;
+import android.arch.background.workmanager.WorkManager;
 import android.arch.background.workmanager.Worker;
 import android.arch.background.workmanager.impl.utils.BaseWorkHelper;
 import android.arch.lifecycle.LiveData;
 import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -38,20 +40,27 @@
 
     private WorkManagerImpl mWorkManagerImpl;
     private String[] mPrerequisiteIds;
+    private String mUniqueTag;
     private List<String> mAllEnqueuedIds = new ArrayList<>();
 
-    WorkContinuationImpl(WorkManagerImpl workManagerImpl, Work[] prerequisiteWork) {
+    WorkContinuationImpl(
+            WorkManagerImpl workManagerImpl, Work[] prerequisiteWork, String uniqueTag) {
         mWorkManagerImpl = workManagerImpl;
         mPrerequisiteIds = new String[prerequisiteWork.length];
         for (int i = 0; i < prerequisiteWork.length; ++i) {
             mPrerequisiteIds[i] = prerequisiteWork[i].getId();
         }
+        mUniqueTag = uniqueTag;
         Collections.addAll(mAllEnqueuedIds, mPrerequisiteIds);
     }
 
     @Override
     public WorkContinuation then(Work... work) {
-        return mWorkManagerImpl.enqueue(work, mPrerequisiteIds);
+        return mWorkManagerImpl.enqueue(
+                work,
+                mPrerequisiteIds,
+                mUniqueTag,
+                WorkManager.KEEP_EXISTING_WORK);
     }
 
     @SafeVarargs
@@ -59,11 +68,18 @@
     public final WorkContinuation then(Class<? extends Worker>... workerClasses) {
         return mWorkManagerImpl.enqueue(
                 BaseWorkHelper.convertWorkerClassArrayToWorkArray(workerClasses),
-                mPrerequisiteIds);
+                mPrerequisiteIds,
+                mUniqueTag,
+                WorkManager.KEEP_EXISTING_WORK);
     }
 
     @Override
     public LiveData<Map<String, Integer>> getStatuses() {
         return mWorkManagerImpl.getStatusesFor(mAllEnqueuedIds);
     }
+
+    @VisibleForTesting
+    String[] getPrerequisiteIds() {
+        return mPrerequisiteIds;
+    }
 }
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkImpl.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkImpl.java
index aebfc1f..b0d1631 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkImpl.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkImpl.java
@@ -115,7 +115,7 @@
         }
 
         @Override
-        public Work.Builder addTag(String tag) {
+        public Work.Builder addTag(@NonNull String tag) {
             mTags.add(tag);
             return this;
         }
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkManagerImpl.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkManagerImpl.java
index 6033855..4ad4dce 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkManagerImpl.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/WorkManagerImpl.java
@@ -39,6 +39,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
 
 import java.util.HashMap;
 import java.util.List;
@@ -69,6 +70,10 @@
         sInstance = new WorkManagerImpl(context, configuration);
     }
 
+    @VisibleForTesting
+    static synchronized void setInstance(@NonNull WorkManagerImpl instance) {
+        sInstance = instance;
+    }
 
     /**
      * Retrieves the singleton instance of {@link WorkManagerImpl}.
@@ -83,7 +88,6 @@
         return sInstance;
     }
 
-
     WorkManagerImpl(Context context, WorkManagerConfiguration configuration) {
         // TODO(janclarin): Move ForegroundProcessor and TaskExecutor to WorkManagerConfiguration.
         // TODO(janclarin): Remove context parameter.
@@ -154,19 +158,36 @@
 
     @Override
     public WorkContinuation enqueue(@NonNull Work... work) {
-        return enqueue(work, null);
+        return enqueue(work, null, null, KEEP_EXISTING_WORK);
     }
 
     @SafeVarargs
     @Override
     public final WorkContinuation enqueue(@NonNull Class<? extends Worker>... workerClasses) {
-        return enqueue(BaseWorkHelper.convertWorkerClassArrayToWorkArray(workerClasses), null);
+        return enqueue(
+                BaseWorkHelper.convertWorkerClassArrayToWorkArray(workerClasses),
+                null,
+                null,
+                KEEP_EXISTING_WORK);
+    }
+
+    @Override
+    public WorkContinuation startSequenceWithUniqueTag(
+            @NonNull String tag,
+            @WorkManager.ExistingWorkPolicy int existingWorkPolicy,
+            @NonNull Work... work) {
+        return enqueue(work, null, tag, existingWorkPolicy);
     }
 
     @Override
     public void enqueue(@NonNull PeriodicWork... periodicWork) {
         mTaskExecutor.executeOnBackgroundThread(
-                new EnqueueRunnable(this, periodicWork, null));
+                new EnqueueRunnable(
+                        this,
+                        periodicWork,
+                        null,
+                        null,
+                        KEEP_EXISTING_WORK));
     }
 
     @Override
@@ -206,11 +227,14 @@
         return mediatorLiveData;
     }
 
-    WorkContinuation enqueue(@NonNull Work[] work, String[] prerequisiteIds) {
-        WorkContinuation workContinuation = new WorkContinuationImpl(this, work);
+    WorkContinuation enqueue(
+            @NonNull Work[] work,
+            String[] prerequisiteIds,
+            String uniqueTag,
+            @WorkManager.ExistingWorkPolicy int existingWorkPolicy) {
+        WorkContinuation workContinuation = new WorkContinuationImpl(this, work, uniqueTag);
         mTaskExecutor.executeOnBackgroundThread(
-                new EnqueueRunnable(this, work, prerequisiteIds));
+                new EnqueueRunnable(this, work, prerequisiteIds, uniqueTag, existingWorkPolicy));
         return workContinuation;
     }
-
 }
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/model/WorkSpecDao.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/model/WorkSpecDao.java
index 6cc5fc3..2a00e5e 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/model/WorkSpecDao.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/model/WorkSpecDao.java
@@ -48,6 +48,14 @@
     void insertWorkSpec(WorkSpec workSpec);
 
     /**
+     * Deletes {@link WorkSpec}s from the database.
+     *
+     * @param workSpecIds The WorkSpec ids to delete.
+     */
+    @Query("DELETE FROM workspec WHERE id IN (:workSpecIds)")
+    void delete(List<String> workSpecIds);
+
+    /**
      * @param id The identifier
      * @return The WorkSpec associated with that id
      */
@@ -64,6 +72,15 @@
     WorkSpec[] getWorkSpecs(List<String> ids);
 
     /**
+     * Retrieves {@link WorkSpec}s with the given tag.
+     *
+     * @param tag The tag of the desired {@link WorkSpec}s.
+     * @return The {@link WorkSpec}s with the requested tag.
+     */
+    @Query("SELECT id FROM workspec WHERE id IN (SELECT work_spec_id FROM worktag WHERE tag=:tag)")
+    List<String> getWorkSpecIdsForTag(String tag);
+
+    /**
      * Updates the status of at least one {@link WorkSpec} by ID.
      *
      * @param status The new status
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/utils/EnqueueRunnable.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/utils/EnqueueRunnable.java
index ee3e972..7473d19 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/utils/EnqueueRunnable.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/utils/EnqueueRunnable.java
@@ -20,6 +20,7 @@
 
 import android.arch.background.workmanager.BaseWork;
 import android.arch.background.workmanager.Work;
+import android.arch.background.workmanager.WorkManager;
 import android.arch.background.workmanager.impl.InternalWorkImpl;
 import android.arch.background.workmanager.impl.WorkDatabase;
 import android.arch.background.workmanager.impl.WorkManagerImpl;
@@ -29,6 +30,10 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
 
 /**
  * A {@link Runnable} to enqueue a {@link Work} in the database.
@@ -38,19 +43,27 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class EnqueueRunnable implements Runnable {
 
+    private static final String TAG = "EnqueueRunnable";
+
     private WorkManagerImpl mWorkManagerImpl;
     private InternalWorkImpl[] mWorkArray;
     private String[] mPrerequisiteIds;
+    private String mUniqueTag;
+    @WorkManager.ExistingWorkPolicy private int mExistingWorkPolicy;
 
     public EnqueueRunnable(WorkManagerImpl workManagerImpl,
             @NonNull BaseWork[] workArray,
-            String[] prerequisiteIds) {
+            String[] prerequisiteIds,
+            String uniqueTag,
+            @WorkManager.ExistingWorkPolicy int existingWorkPolicy) {
         mWorkManagerImpl = workManagerImpl;
         mWorkArray = new InternalWorkImpl[workArray.length];
         for (int i = 0; i < workArray.length; ++i) {
             mWorkArray[i] = (InternalWorkImpl) workArray[i];
         }
         mPrerequisiteIds = prerequisiteIds;
+        mUniqueTag = uniqueTag;
+        mExistingWorkPolicy = existingWorkPolicy;
     }
 
     @WorkerThread
@@ -62,6 +75,36 @@
             long currentTimeMillis = System.currentTimeMillis();
             boolean hasPrerequisite = (mPrerequisiteIds != null && mPrerequisiteIds.length > 0);
 
+            if (hasPrerequisite) {
+                // If there are prerequisites, make sure they actually exist before enqueuing
+                // anything.  Prerequisites may not exist if we are using unique tags, because the
+                // chain of work could have been wiped out already.
+                for (String id : mPrerequisiteIds) {
+                    if (workDatabase.workSpecDao().getWorkSpec(id) == null) {
+                        Log.e(TAG, "Prerequisite " + id + " doesn't exist; not enqueuing");
+                        return;
+                    }
+                }
+            }
+
+            boolean hasUniqueTag = !TextUtils.isEmpty(mUniqueTag);
+            if (hasUniqueTag && !hasPrerequisite) {
+                List<String> existingWorkSpecIds =
+                        workDatabase.workSpecDao().getWorkSpecIdsForTag(mUniqueTag);
+                if (!existingWorkSpecIds.isEmpty()) {
+                    if (mExistingWorkPolicy == WorkManager.KEEP_EXISTING_WORK) {
+                        return;
+                    }
+
+                    // Cancel all of these workers.
+                    CancelWorkRunnable cancelWorkRunnable =
+                            new CancelWorkRunnable(mWorkManagerImpl, null, mUniqueTag);
+                    cancelWorkRunnable.run();
+                    // And delete all the database records.
+                    workDatabase.workSpecDao().delete(existingWorkSpecIds);
+                }
+            }
+
             for (InternalWorkImpl work : mWorkArray) {
                 WorkSpec workSpec = work.getWorkSpec();
 
@@ -85,6 +128,11 @@
                 for (String tag : work.getTags()) {
                     workDatabase.workTagDao().insert(new WorkTag(tag, work.getId()));
                 }
+
+                // Enforce that the unique tag is always present.
+                if (hasUniqueTag) {
+                    workDatabase.workTagDao().insert(new WorkTag(mUniqueTag, work.getId()));
+                }
             }
             workDatabase.setTransactionSuccessful();