1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.work.impl;
18
19import android.arch.lifecycle.LiveData;
20import android.os.Looper;
21import android.support.annotation.NonNull;
22import android.support.annotation.Nullable;
23import android.support.annotation.RestrictTo;
24import android.support.annotation.WorkerThread;
25import android.text.TextUtils;
26import android.util.Log;
27
28import androidx.work.ArrayCreatingInputMerger;
29import androidx.work.ExistingWorkPolicy;
30import androidx.work.OneTimeWorkRequest;
31import androidx.work.SynchronousWorkContinuation;
32import androidx.work.WorkContinuation;
33import androidx.work.WorkRequest;
34import androidx.work.WorkStatus;
35import androidx.work.impl.utils.EnqueueRunnable;
36import androidx.work.impl.workers.CombineContinuationsWorker;
37
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.HashSet;
41import java.util.List;
42import java.util.Set;
43
44/**
45 * A concrete implementation of {@link WorkContinuation}.
46 *
47 * @hide
48 */
49@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
50public class WorkContinuationImpl extends WorkContinuation
51        implements SynchronousWorkContinuation {
52
53    private static final String TAG = "WorkContinuationImpl";
54
55    private final WorkManagerImpl mWorkManagerImpl;
56    private final String mName;
57    private final ExistingWorkPolicy mExistingWorkPolicy;
58    private final List<? extends WorkRequest> mWork;
59    private final List<String> mIds;
60    private final List<String> mAllIds;
61    private final List<WorkContinuationImpl> mParents;
62    private boolean mEnqueued;
63
64    @NonNull
65    public WorkManagerImpl getWorkManagerImpl() {
66        return mWorkManagerImpl;
67    }
68
69    @Nullable
70    public String getName() {
71        return mName;
72    }
73
74    public ExistingWorkPolicy getExistingWorkPolicy() {
75        return mExistingWorkPolicy;
76    }
77
78    @NonNull
79    public List<? extends WorkRequest> getWork() {
80        return mWork;
81    }
82
83    @NonNull
84    public List<String> getIds() {
85        return mIds;
86    }
87
88    public List<String> getAllIds() {
89        return mAllIds;
90    }
91
92    public boolean isEnqueued() {
93        return mEnqueued;
94    }
95
96    /**
97     * Marks the {@link WorkContinuationImpl} as enqueued.
98     */
99    public void markEnqueued() {
100        mEnqueued = true;
101    }
102
103    public List<WorkContinuationImpl> getParents() {
104        return mParents;
105    }
106
107    WorkContinuationImpl(
108            @NonNull WorkManagerImpl workManagerImpl,
109            @NonNull List<? extends WorkRequest> work) {
110        this(
111                workManagerImpl,
112                null,
113                ExistingWorkPolicy.KEEP,
114                work,
115                null);
116    }
117
118    WorkContinuationImpl(
119            @NonNull WorkManagerImpl workManagerImpl,
120            String name,
121            ExistingWorkPolicy existingWorkPolicy,
122            @NonNull List<? extends WorkRequest> work) {
123        this(workManagerImpl, name, existingWorkPolicy, work, null);
124    }
125
126    WorkContinuationImpl(@NonNull WorkManagerImpl workManagerImpl,
127            String name,
128            ExistingWorkPolicy existingWorkPolicy,
129            @NonNull List<? extends WorkRequest> work,
130            @Nullable List<WorkContinuationImpl> parents) {
131        mWorkManagerImpl = workManagerImpl;
132        mName = name;
133        mExistingWorkPolicy = existingWorkPolicy;
134        mWork = work;
135        mParents = parents;
136        mIds = new ArrayList<>(mWork.size());
137        mAllIds = new ArrayList<>();
138        if (parents != null) {
139            for (WorkContinuationImpl parent : parents) {
140                mAllIds.addAll(parent.mAllIds);
141            }
142        }
143        for (int i = 0; i < work.size(); i++) {
144            String id = work.get(i).getStringId();
145            mIds.add(id);
146            mAllIds.add(id);
147        }
148    }
149
150    @Override
151    public WorkContinuation then(List<OneTimeWorkRequest> work) {
152        // TODO (rahulrav@) We need to decide if we want to allow chaining of continuations after
153        // an initial call to enqueue()
154        return new WorkContinuationImpl(mWorkManagerImpl,
155                mName,
156                ExistingWorkPolicy.KEEP,
157                work,
158                Collections.singletonList(this));
159    }
160
161    @Override
162    public LiveData<List<WorkStatus>> getStatuses() {
163        return mWorkManagerImpl.getStatusesById(mAllIds);
164    }
165
166    @Override
167    public List<WorkStatus> getStatusesSync() {
168        if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
169            throw new IllegalStateException("Cannot getStatusesSync on main thread!");
170        }
171        return mWorkManagerImpl.getStatusesByIdSync(mAllIds);
172    }
173
174    @Override
175    public void enqueue() {
176        // Only enqueue if not already enqueued.
177        if (!mEnqueued) {
178            // The runnable walks the hierarchy of the continuations
179            // and marks them enqueued using the markEnqueued() method, parent first.
180            mWorkManagerImpl.getTaskExecutor()
181                    .executeOnBackgroundThread(new EnqueueRunnable(this));
182        } else {
183            Log.w(TAG, String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
184        }
185    }
186
187    @Override
188    @WorkerThread
189    public void enqueueSync() {
190        if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
191            throw new IllegalStateException("Cannot enqueueSync on main thread!");
192        }
193
194        if (!mEnqueued) {
195            // The runnable walks the hierarchy of the continuations
196            // and marks them enqueued using the markEnqueued() method, parent first.
197            new EnqueueRunnable(this).run();
198        } else {
199            Log.w(TAG, String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
200        }
201    }
202
203    @Override
204    public SynchronousWorkContinuation synchronous() {
205        return this;
206    }
207
208    @Override
209    protected WorkContinuation combineInternal(
210            @Nullable OneTimeWorkRequest work,
211            @NonNull List<WorkContinuation> continuations) {
212
213        if (work == null) {
214            work = new OneTimeWorkRequest.Builder(CombineContinuationsWorker.class)
215                    .setInputMerger(ArrayCreatingInputMerger.class)
216                    .build();
217        }
218
219        List<WorkContinuationImpl> parents = new ArrayList<>(continuations.size());
220        for (WorkContinuation continuation : continuations) {
221            parents.add((WorkContinuationImpl) continuation);
222        }
223
224        return new WorkContinuationImpl(mWorkManagerImpl,
225                null,
226                ExistingWorkPolicy.KEEP,
227                Collections.singletonList(work),
228                parents);
229    }
230
231    /**
232     * @return {@code true} If there are cycles in the {@link WorkContinuationImpl}.
233
234     * @hide
235     */
236    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
237    public boolean hasCycles() {
238        return hasCycles(this, new HashSet<String>());
239    }
240
241    /**
242     * @param continuation The {@link WorkContinuationImpl} instance.
243     * @param visited      The {@link Set} of {@link androidx.work.impl.model.WorkSpec} ids
244     *                     marked as visited.
245     * @return {@code true} if the {@link WorkContinuationImpl} has a cycle.
246     * @hide
247     */
248    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
249    private static boolean hasCycles(
250            @NonNull WorkContinuationImpl continuation,
251            @NonNull Set<String> visited) {
252
253        // mark the ids of this workContinuation as visited
254        // before we check if the parents have cycles.
255        visited.addAll(continuation.getIds());
256
257        Set<String> prerequisiteIds = prerequisitesFor(continuation);
258        for (String id : visited) {
259            if (prerequisiteIds.contains(id)) {
260                // This prerequisite has already been visited before.
261                // There is a cycle.
262                return true;
263            }
264        }
265
266        List<WorkContinuationImpl> parents = continuation.getParents();
267        if (parents != null && !parents.isEmpty()) {
268            for (WorkContinuationImpl parent : parents) {
269                // if any of the parent has a cycle, then bail out
270                if (hasCycles(parent, visited)) {
271                    return true;
272                }
273            }
274        }
275
276        // Un-mark the ids of the workContinuation as visited for the next parent.
277        // This is because we don't want to change the state of visited ids for subsequent parents
278        // This is being done to avoid allocations. Ideally we would check for a
279        // hasCycles(parent, new HashSet<>(visited)) instead.
280        visited.removeAll(continuation.getIds());
281        return false;
282    }
283
284    /**
285     * @return the {@link Set} of pre-requisites for a given {@link WorkContinuationImpl}.
286     *
287     * @hide
288     */
289    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
290    public static Set<String> prerequisitesFor(WorkContinuationImpl continuation) {
291        Set<String> preRequisites = new HashSet<>();
292        List<WorkContinuationImpl> parents = continuation.getParents();
293        if (parents != null && !parents.isEmpty()) {
294            for (WorkContinuationImpl parent : parents) {
295                preRequisites.addAll(parent.getIds());
296            }
297        }
298        return preRequisites;
299    }
300}
301