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