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();