Add a WorkContinuation.join(...) which adds the ability to do more advanced continuations.
Test: Added tests for the new join() APIs.

Change-Id: I06e7ffbae7518d6a79ba1e5c24185a542a2b7c0e
diff --git a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkContinuationImplTest.java b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkContinuationImplTest.java
index f0c3a22..81387c9 100644
--- a/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkContinuationImplTest.java
+++ b/background/workmanager/src/androidTest/java/android/arch/background/workmanager/impl/WorkContinuationImplTest.java
@@ -27,6 +27,7 @@
 
 import android.arch.background.workmanager.TestLifecycleOwner;
 import android.arch.background.workmanager.Work;
+import android.arch.background.workmanager.WorkContinuation;
 import android.arch.background.workmanager.executors.SynchronousExecutorService;
 import android.arch.background.workmanager.impl.utils.taskexecutor.InstantTaskExecutorRule;
 import android.arch.background.workmanager.worker.TestWorker;
@@ -106,9 +107,9 @@
         WorkContinuationImpl continuation =
                 new WorkContinuationImpl(mWorkManagerImpl, testWork);
 
-        assertThat(continuation.getParent(), is(nullValue()));
-        assertThat(continuation.getIds().length, is(1));
-        assertThat(continuation.getIds()[0], is(testWork.getId()));
+        assertThat(continuation.getParents(), is(nullValue()));
+        assertThat(continuation.getIds().size(), is(1));
+        assertThat(continuation.getIds().get(0), is(testWork.getId()));
         assertThat(continuation.getAllIds().size(), is(1));
     }
 
@@ -121,10 +122,9 @@
         WorkContinuationImpl dependent = (WorkContinuationImpl) (continuation.then(
                 dependentWork));
 
-        assertThat(dependent.getParent(), is(notNullValue()));
-        assertThat(dependent.getParent(), is(continuation));
-        assertThat(dependent.getIds().length, is(1));
-        assertThat(dependent.getIds()[0], is(dependentWork.getId()));
+        assertThat(dependent.getParents(), containsInAnyOrder(continuation));
+        assertThat(dependent.getIds().size(), is(1));
+        assertThat(dependent.getIds().get(0), is(dependentWork.getId()));
         assertThat(dependent.getAllIds().size(), is(2));
         assertThat(
                 dependent.getAllIds(),
@@ -167,12 +167,65 @@
         verify(spy, times(0)).markEnqueued();
     }
 
+    @Test
+    public void testContinuation_join() {
+        WorkContinuationImpl first = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl second = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl dependent = (WorkContinuationImpl) WorkContinuation.join(first,
+                second);
+        assertThat(dependent.getParents(), is(notNullValue()));
+        assertThat(dependent.getParents(), containsInAnyOrder(first, second));
+    }
+
+    @Test
+    public void testContinuation_joinAndEnqueue() {
+        WorkContinuationImpl first = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl second = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+
+        WorkContinuationImpl third = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl fourth = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+
+        WorkContinuationImpl firstDependent = (WorkContinuationImpl) WorkContinuation.join(first,
+                second);
+        WorkContinuationImpl secondDependent = (WorkContinuationImpl) WorkContinuation.join(third,
+                fourth);
+        WorkContinuationImpl dependent = (WorkContinuationImpl) WorkContinuation.join(
+                firstDependent, secondDependent);
+        dependent.enqueue();
+        verifyEnqueued(dependent);
+    }
+
+    @Test
+    public void testContinuation_joinAndEnqueueWithOverlaps() {
+        WorkContinuationImpl first = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl second = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl third = new WorkContinuationImpl(mWorkManagerImpl,
+                createTestWorker());
+        WorkContinuationImpl firstDependent = (WorkContinuationImpl) WorkContinuation.join(first,
+                second);
+        WorkContinuationImpl secondDependent = (WorkContinuationImpl) WorkContinuation.join(first,
+                third);
+        WorkContinuationImpl dependent = (WorkContinuationImpl) WorkContinuation.join(
+                firstDependent, secondDependent);
+        dependent.enqueue();
+        verifyEnqueued(dependent);
+    }
+
     private void verifyEnqueued(WorkContinuationImpl continuation) {
         assertThat(continuation.isEnqueued(), is(true));
-        WorkContinuationImpl parent = continuation.getParent();
-        while (parent != null) {
-            assertThat(parent.isEnqueued(), is(true));
-            parent = parent.getParent();
+        List<WorkContinuationImpl> parents = continuation.getParents();
+        if (parents != null) {
+            for (WorkContinuationImpl parent : parents) {
+                verifyEnqueued(parent);
+            }
         }
     }
 
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/WorkContinuation.java b/background/workmanager/src/main/java/android/arch/background/workmanager/WorkContinuation.java
index 958e07d..3ac8624 100644
--- a/background/workmanager/src/main/java/android/arch/background/workmanager/WorkContinuation.java
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/WorkContinuation.java
@@ -16,6 +16,7 @@
 package android.arch.background.workmanager;
 
 import android.arch.lifecycle.LiveData;
+import android.support.annotation.NonNull;
 
 import java.util.Arrays;
 import java.util.List;
@@ -65,5 +66,23 @@
      */
     public abstract void enqueue();
 
+    /**
+     * Joins multiple {@link WorkContinuation}s to allow for complex chaining.
+     *
+     * @param continuations Two or more {@link WorkContinuation}s that are prerequisites for the
+     *                      return value
+     * @return A {@link WorkContinuation} that allows further chaining
+     */
+    public static WorkContinuation join(@NonNull WorkContinuation... continuations) {
+        if (continuations.length < 2) {
+            throw new IllegalArgumentException(
+                    "WorkContinuation.join() needs at least 2 continuations.");
+        }
+
+        return continuations[0].joinInternal(continuations);
+    }
+
     protected abstract WorkContinuation then(List<Class<? extends Worker>> workerClasses);
+
+    protected abstract WorkContinuation joinInternal(@NonNull WorkContinuation... continuations);
 }
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 3183532..f3b8525 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
@@ -23,6 +23,7 @@
 import android.arch.background.workmanager.Worker;
 import android.arch.background.workmanager.impl.utils.BaseWorkHelper;
 import android.arch.background.workmanager.impl.utils.EnqueueRunnable;
+import android.arch.background.workmanager.impl.workers.JoinWorker;
 import android.arch.lifecycle.LiveData;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -31,6 +32,7 @@
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -48,10 +50,10 @@
     private final String mUniqueTag;
     private final @WorkManager.ExistingWorkPolicy int mExistingWorkPolicy;
     private final BaseWork[] mWork;
-    private final String[] mIds;
+    private final List<String> mIds;
     private final List<String> mAllIds;
+    private final List<WorkContinuationImpl> mParents;
     private boolean mEnqueued;
-    private WorkContinuationImpl mParent;
 
     @NonNull
     public WorkManagerImpl getWorkManagerImpl() {
@@ -73,7 +75,7 @@
     }
 
     @NonNull
-    public String[] getIds() {
+    public List<String> getIds() {
         return mIds;
     }
 
@@ -92,40 +94,42 @@
         mEnqueued = true;
     }
 
-    public WorkContinuationImpl getParent() {
-        return mParent;
+    public List<WorkContinuationImpl> getParents() {
+        return mParents;
     }
 
-    WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl, @NonNull BaseWork... work) {
+    WorkContinuationImpl(
+            @NonNull WorkManagerImpl workManagerImpl,
+            @NonNull BaseWork... work) {
         this(workManagerImpl, null, WorkManager.KEEP_EXISTING_WORK, work, null);
     }
 
     WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
-            @NonNull String uniqueTag,
+            String uniqueTag,
             @WorkManager.ExistingWorkPolicy int existingWorkPolicy,
             @NonNull BaseWork... work) {
         this(workManagerImpl, uniqueTag, existingWorkPolicy, work, null);
     }
 
-    private WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
+    WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
             String uniqueTag,
             @WorkManager.ExistingWorkPolicy int existingWorkPolicy,
             @NonNull BaseWork[] work,
-            @Nullable WorkContinuationImpl parent) {
+            @Nullable List<WorkContinuationImpl> parents) {
         mWorkManagerImpl = workManagerImpl;
         mUniqueTag = uniqueTag;
         mExistingWorkPolicy = existingWorkPolicy;
         mWork = work;
-        mParent = parent;
-
-        final int parentSize = mParent != null ? mParent.mAllIds.size() : 0;
-        mIds = new String[mWork.length];
-        mAllIds = new ArrayList<>(mWork.length + parentSize);
-        if (parent != null) {
-            mAllIds.addAll(mParent.mAllIds);
+        mParents = parents;
+        mIds = new ArrayList<>(mWork.length);
+        mAllIds = new ArrayList<>();
+        if (parents != null) {
+            for (WorkContinuationImpl parent : parents) {
+                mAllIds.addAll(parent.mAllIds);
+            }
         }
         for (int i = 0; i < work.length; i++) {
-            mIds[i] = work[i].getId();
+            mIds.add(work[i].getId());
             mAllIds.add(work[i].getId());
         }
     }
@@ -138,7 +142,7 @@
                 mUniqueTag,
                 WorkManager.KEEP_EXISTING_WORK,
                 work,
-                this);
+                Collections.singletonList(this));
     }
 
     @Override
@@ -147,7 +151,7 @@
                 mUniqueTag,
                 WorkManager.KEEP_EXISTING_WORK,
                 BaseWorkHelper.convertWorkerClassListToWorkArray(workerClasses),
-                this);
+                Collections.singletonList(this));
     }
 
     @Override
@@ -168,4 +172,19 @@
                     String.format("Already enqueued work ids (%s).", TextUtils.join(", ", mIds)));
         }
     }
+
+    @Override
+    protected WorkContinuation joinInternal(@NonNull WorkContinuation... continuations) {
+        List<WorkContinuationImpl> parents = new ArrayList<>(continuations.length);
+        for (WorkContinuation continuation : continuations) {
+            parents.add((WorkContinuationImpl) continuation);
+        }
+
+        Work work = Work.newBuilder(JoinWorker.class).build();
+        return new WorkContinuationImpl(mWorkManagerImpl,
+                null,
+                WorkManager.KEEP_EXISTING_WORK,
+                new BaseWork[]{work},
+                parents);
+    }
 }
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 bb14f31..e1c0541 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
@@ -36,7 +36,9 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Manages the enqueuing of a {@link WorkContinuationImpl}.
@@ -96,19 +98,20 @@
             @NonNull WorkContinuationImpl workContinuation,
             @NonNull List<InternalWorkImpl> workToBeScheduled) {
 
-        WorkContinuationImpl parent = workContinuation.getParent();
-        if (parent != null) {
-            // When chaining off a completed continuation we need to pay
-            // attention to parents that may have been marked as enqueued before.
-            if (!parent.isEnqueued()) {
-                processContinuation(parent, workToBeScheduled);
-            } else {
-                Log.w(TAG,
-                        String.format(
-                                "Already enqueued work ids (%s).",
-                                TextUtils.join(", ", parent.getIds())));
+        List<WorkContinuationImpl> parents = workContinuation.getParents();
+        if (parents != null) {
+            for (WorkContinuationImpl parent : parents) {
+                // When chaining off a completed continuation we need to pay
+                // attention to parents that may have been marked as enqueued before.
+                if (!parent.isEnqueued()) {
+                    processContinuation(parent, workToBeScheduled);
+                } else {
+                    Log.w(TAG,
+                            String.format(
+                                    "Already enqueued work ids (%s).",
+                                    TextUtils.join(", ", parent.getIds())));
+                }
             }
-
         }
         enqueueContinuation(workContinuation, workToBeScheduled);
     }
@@ -117,13 +120,18 @@
             @NonNull WorkContinuationImpl workContinuation,
             @NonNull List<InternalWorkImpl> workToBeScheduled) {
 
-        WorkContinuationImpl parent = workContinuation.getParent();
-        String[] prerequisiteIds = parent != null ? parent.getIds() : null;
+        List<WorkContinuationImpl> parents = workContinuation.getParents();
+        Set<String> prerequisiteIds = new HashSet<>();
+        if (parents != null) {
+            for (WorkContinuationImpl parent : parents) {
+                prerequisiteIds.addAll(parent.getIds());
+            }
+        }
 
         enqueueWorkWithPrerequisites(
                 workContinuation.getWorkManagerImpl(),
                 workContinuation.getWork(),
-                prerequisiteIds,
+                prerequisiteIds.toArray(new String[0]),
                 workContinuation.getUniqueTag(),
                 workContinuation.getExistingWorkPolicy(),
                 workToBeScheduled);
diff --git a/background/workmanager/src/main/java/android/arch/background/workmanager/impl/workers/JoinWorker.java b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/workers/JoinWorker.java
new file mode 100644
index 0000000..a8038eb
--- /dev/null
+++ b/background/workmanager/src/main/java/android/arch/background/workmanager/impl/workers/JoinWorker.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 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.arch.background.workmanager.impl.workers;
+
+import android.arch.background.workmanager.Worker;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Is a implementation of the {@link android.arch.background.workmanager.Worker} that helps join
+ * work continuations.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JoinWorker extends Worker {
+    @Override
+    public int doWork() {
+        return WORKER_RESULT_SUCCESS;
+    }
+}