1 /* 2 * Copyright 2017 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 17 package androidx.work.impl; 18 19 import static androidx.work.State.CANCELLED; 20 import static androidx.work.State.ENQUEUED; 21 import static androidx.work.State.FAILED; 22 import static androidx.work.State.RUNNING; 23 import static androidx.work.State.SUCCEEDED; 24 25 import android.content.Context; 26 import android.support.annotation.NonNull; 27 import android.support.annotation.Nullable; 28 import android.support.annotation.RestrictTo; 29 import android.support.annotation.VisibleForTesting; 30 import android.support.annotation.WorkerThread; 31 import android.util.Log; 32 33 import androidx.work.Configuration; 34 import androidx.work.Data; 35 import androidx.work.InputMerger; 36 import androidx.work.State; 37 import androidx.work.Worker; 38 import androidx.work.impl.model.DependencyDao; 39 import androidx.work.impl.model.WorkSpec; 40 import androidx.work.impl.model.WorkSpecDao; 41 import androidx.work.impl.model.WorkTagDao; 42 import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor; 43 44 import java.lang.reflect.Method; 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.UUID; 48 49 /** 50 * A runnable that looks up the {@link WorkSpec} from the database for a given id, instantiates 51 * its Worker, and then calls it. 52 * 53 * @hide 54 */ 55 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 56 public class WorkerWrapper implements Runnable { 57 58 private static final String TAG = "WorkerWrapper"; 59 private Context mAppContext; 60 private String mWorkSpecId; 61 private ExecutionListener mListener; 62 private List<Scheduler> mSchedulers; 63 private Extras.RuntimeExtras mRuntimeExtras; 64 private WorkSpec mWorkSpec; 65 Worker mWorker; 66 67 private Configuration mConfiguration; 68 private WorkDatabase mWorkDatabase; 69 private WorkSpecDao mWorkSpecDao; 70 private DependencyDao mDependencyDao; 71 private WorkTagDao mWorkTagDao; 72 73 private volatile boolean mInterrupted; 74 WorkerWrapper(Builder builder)75 private WorkerWrapper(Builder builder) { 76 mAppContext = builder.mAppContext; 77 mWorkSpecId = builder.mWorkSpecId; 78 mListener = builder.mListener; 79 mSchedulers = builder.mSchedulers; 80 mRuntimeExtras = builder.mRuntimeExtras; 81 mWorker = builder.mWorker; 82 83 mConfiguration = builder.mConfiguration; 84 mWorkDatabase = builder.mWorkDatabase; 85 mWorkSpecDao = mWorkDatabase.workSpecDao(); 86 mDependencyDao = mWorkDatabase.dependencyDao(); 87 mWorkTagDao = mWorkDatabase.workTagDao(); 88 } 89 90 @WorkerThread 91 @Override run()92 public void run() { 93 if (tryCheckForInterruptionAndNotify()) { 94 return; 95 } 96 97 mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId); 98 if (mWorkSpec == null) { 99 Log.e(TAG, String.format("Didn't find WorkSpec for id %s", mWorkSpecId)); 100 notifyListener(false, false); 101 return; 102 } 103 104 // Do a quick check to make sure we don't need to bail out in case this work is already 105 // running, finished, or is blocked. 106 if (mWorkSpec.state != ENQUEUED) { 107 notifyIncorrectStatus(); 108 return; 109 } 110 111 // Merge inputs. This can be potentially expensive code, so this should not be done inside 112 // a database transaction. 113 Data input; 114 if (mWorkSpec.isPeriodic()) { 115 input = mWorkSpec.input; 116 } else { 117 InputMerger inputMerger = InputMerger.fromClassName(mWorkSpec.inputMergerClassName); 118 if (inputMerger == null) { 119 Log.e(TAG, String.format("Could not create Input Merger %s", 120 mWorkSpec.inputMergerClassName)); 121 setFailedAndNotify(); 122 return; 123 } 124 List<Data> inputs = new ArrayList<>(); 125 inputs.add(mWorkSpec.input); 126 inputs.addAll(mWorkSpecDao.getInputsFromPrerequisites(mWorkSpecId)); 127 input = inputMerger.merge(inputs); 128 } 129 130 Extras extras = new Extras( 131 input, 132 mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId), 133 mRuntimeExtras, 134 mWorkSpec.runAttemptCount); 135 136 // Not always creating a worker here, as the WorkerWrapper.Builder can set a worker override 137 // in test mode. 138 if (mWorker == null) { 139 mWorker = workerFromWorkSpec(mAppContext, mWorkSpec, extras); 140 } 141 142 if (mWorker == null) { 143 Log.e(TAG, String.format("Could for create Worker %s", mWorkSpec.workerClassName)); 144 setFailedAndNotify(); 145 return; 146 } 147 148 // Try to set the work to the running state. Note that this may fail because another thread 149 // may have modified the DB since we checked last at the top of this function. 150 if (trySetRunning()) { 151 if (tryCheckForInterruptionAndNotify()) { 152 return; 153 } 154 155 Worker.Result result; 156 try { 157 result = mWorker.doWork(); 158 } catch (Exception | Error e) { 159 result = Worker.Result.FAILURE; 160 } 161 162 try { 163 mWorkDatabase.beginTransaction(); 164 if (!tryCheckForInterruptionAndNotify()) { 165 State state = mWorkSpecDao.getState(mWorkSpecId); 166 if (state == null) { 167 // state can be null here with a REPLACE on beginUniqueWork(). 168 // Treat it as a failure, and rescheduleAndNotify() will 169 // turn into a no-op. We still need to notify potential observers 170 // holding on to wake locks on our behalf. 171 notifyListener(false, false); 172 } else if (state == RUNNING) { 173 handleResult(result); 174 } else if (!state.isFinished()) { 175 rescheduleAndNotify(); 176 } 177 mWorkDatabase.setTransactionSuccessful(); 178 } 179 } finally { 180 mWorkDatabase.endTransaction(); 181 } 182 } else { 183 notifyIncorrectStatus(); 184 } 185 } 186 187 /** 188 * @hide 189 */ 190 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) interrupt(boolean cancelled)191 public void interrupt(boolean cancelled) { 192 mInterrupted = true; 193 // Worker can be null if run() hasn't been called yet. 194 if (mWorker != null) { 195 mWorker.stop(cancelled); 196 } 197 } 198 notifyIncorrectStatus()199 private void notifyIncorrectStatus() { 200 State status = mWorkSpecDao.getState(mWorkSpecId); 201 if (status == RUNNING) { 202 Log.d(TAG, String.format("Status for %s is RUNNING;" 203 + "not doing any work and rescheduling for later execution", mWorkSpecId)); 204 notifyListener(false, true); 205 } else { 206 Log.e(TAG, 207 String.format("Status for %s is %s; not doing any work", mWorkSpecId, status)); 208 notifyListener(false, false); 209 } 210 } 211 tryCheckForInterruptionAndNotify()212 private boolean tryCheckForInterruptionAndNotify() { 213 if (mInterrupted) { 214 Log.d(TAG, String.format("Work interrupted for %s", mWorkSpecId)); 215 State currentState = mWorkSpecDao.getState(mWorkSpecId); 216 if (currentState == null) { 217 // This can happen because of a beginUniqueWork(..., REPLACE, ...). Notify the 218 // listeners so we can clean up any wake locks, etc. 219 notifyListener(false, false); 220 } else { 221 notifyListener(currentState == SUCCEEDED, !currentState.isFinished()); 222 } 223 return true; 224 } 225 return false; 226 } 227 notifyListener(final boolean isSuccessful, final boolean needsReschedule)228 private void notifyListener(final boolean isSuccessful, final boolean needsReschedule) { 229 if (mListener == null) { 230 return; 231 } 232 WorkManagerTaskExecutor.getInstance().postToMainThread(new Runnable() { 233 @Override 234 public void run() { 235 mListener.onExecuted(mWorkSpecId, isSuccessful, needsReschedule); 236 } 237 }); 238 } 239 handleResult(Worker.Result result)240 private void handleResult(Worker.Result result) { 241 switch (result) { 242 case SUCCESS: { 243 Log.d(TAG, String.format("Worker result SUCCESS for %s", mWorkSpecId)); 244 if (mWorkSpec.isPeriodic()) { 245 resetPeriodicAndNotify(true); 246 } else { 247 setSucceededAndNotify(); 248 } 249 break; 250 } 251 252 case RETRY: { 253 Log.d(TAG, String.format("Worker result RETRY for %s", mWorkSpecId)); 254 rescheduleAndNotify(); 255 break; 256 } 257 258 case FAILURE: 259 default: { 260 Log.d(TAG, String.format("Worker result FAILURE for %s", mWorkSpecId)); 261 if (mWorkSpec.isPeriodic()) { 262 resetPeriodicAndNotify(false); 263 } else { 264 setFailedAndNotify(); 265 } 266 } 267 } 268 } 269 trySetRunning()270 private boolean trySetRunning() { 271 boolean setToRunning = false; 272 mWorkDatabase.beginTransaction(); 273 try { 274 State currentState = mWorkSpecDao.getState(mWorkSpecId); 275 if (currentState == ENQUEUED) { 276 mWorkSpecDao.setState(RUNNING, mWorkSpecId); 277 mWorkSpecDao.incrementWorkSpecRunAttemptCount(mWorkSpecId); 278 mWorkDatabase.setTransactionSuccessful(); 279 setToRunning = true; 280 } 281 } finally { 282 mWorkDatabase.endTransaction(); 283 } 284 return setToRunning; 285 } 286 setFailedAndNotify()287 private void setFailedAndNotify() { 288 mWorkDatabase.beginTransaction(); 289 try { 290 recursivelyFailWorkAndDependents(mWorkSpecId); 291 292 // Try to set the output for the failed work but check if the worker exists; this could 293 // be a permanent error where we couldn't find or create the worker class. 294 if (mWorker != null) { 295 // Update Data as necessary. 296 Data output = mWorker.getOutputData(); 297 mWorkSpecDao.setOutput(mWorkSpecId, output); 298 } 299 300 mWorkDatabase.setTransactionSuccessful(); 301 } finally { 302 mWorkDatabase.endTransaction(); 303 notifyListener(false, false); 304 } 305 306 Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers); 307 } 308 recursivelyFailWorkAndDependents(String workSpecId)309 private void recursivelyFailWorkAndDependents(String workSpecId) { 310 List<String> dependentIds = mDependencyDao.getDependentWorkIds(workSpecId); 311 for (String id : dependentIds) { 312 recursivelyFailWorkAndDependents(id); 313 } 314 315 // Don't fail already cancelled work. 316 if (mWorkSpecDao.getState(workSpecId) != CANCELLED) { 317 mWorkSpecDao.setState(FAILED, workSpecId); 318 } 319 } 320 rescheduleAndNotify()321 private void rescheduleAndNotify() { 322 mWorkDatabase.beginTransaction(); 323 try { 324 mWorkSpecDao.setState(ENQUEUED, mWorkSpecId); 325 // TODO(xbhatnag): Period Start Time is confusing for non-periodic work. Rename. 326 mWorkSpecDao.setPeriodStartTime(mWorkSpecId, System.currentTimeMillis()); 327 mWorkDatabase.setTransactionSuccessful(); 328 } finally { 329 mWorkDatabase.endTransaction(); 330 notifyListener(false, true); 331 } 332 } 333 resetPeriodicAndNotify(boolean isSuccessful)334 private void resetPeriodicAndNotify(boolean isSuccessful) { 335 mWorkDatabase.beginTransaction(); 336 try { 337 long currentPeriodStartTime = mWorkSpec.periodStartTime; 338 long nextPeriodStartTime = currentPeriodStartTime + mWorkSpec.intervalDuration; 339 mWorkSpecDao.setPeriodStartTime(mWorkSpecId, nextPeriodStartTime); 340 mWorkSpecDao.setState(ENQUEUED, mWorkSpecId); 341 mWorkSpecDao.resetWorkSpecRunAttemptCount(mWorkSpecId); 342 mWorkDatabase.setTransactionSuccessful(); 343 } finally { 344 mWorkDatabase.endTransaction(); 345 notifyListener(isSuccessful, false); 346 } 347 } 348 setSucceededAndNotify()349 private void setSucceededAndNotify() { 350 mWorkDatabase.beginTransaction(); 351 try { 352 mWorkSpecDao.setState(SUCCEEDED, mWorkSpecId); 353 354 // Update Data as necessary. 355 Data output = mWorker.getOutputData(); 356 mWorkSpecDao.setOutput(mWorkSpecId, output); 357 358 // Unblock Dependencies and set Period Start Time 359 long currentTimeMillis = System.currentTimeMillis(); 360 List<String> dependentWorkIds = mDependencyDao.getDependentWorkIds(mWorkSpecId); 361 for (String dependentWorkId : dependentWorkIds) { 362 if (mDependencyDao.hasCompletedAllPrerequisites(dependentWorkId)) { 363 Log.d(TAG, String.format("Setting status to enqueued for %s", dependentWorkId)); 364 mWorkSpecDao.setState(ENQUEUED, dependentWorkId); 365 mWorkSpecDao.setPeriodStartTime(dependentWorkId, currentTimeMillis); 366 } 367 } 368 369 mWorkDatabase.setTransactionSuccessful(); 370 } finally { 371 mWorkDatabase.endTransaction(); 372 notifyListener(true, false); 373 } 374 375 // This takes of scheduling the dependent workers as they have been marked ENQUEUED. 376 Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers); 377 } 378 workerFromWorkSpec(@onNull Context context, @NonNull WorkSpec workSpec, @NonNull Extras extras)379 static Worker workerFromWorkSpec(@NonNull Context context, 380 @NonNull WorkSpec workSpec, 381 @NonNull Extras extras) { 382 String workerClassName = workSpec.workerClassName; 383 UUID workSpecId = UUID.fromString(workSpec.id); 384 return workerFromClassName( 385 context, 386 workerClassName, 387 workSpecId, 388 extras); 389 } 390 391 /** 392 * Creates a {@link Worker} reflectively & initializes the worker. 393 * 394 * @param context The application {@link Context} 395 * @param workerClassName The fully qualified class name for the {@link Worker} 396 * @param workSpecId The {@link WorkSpec} identifier 397 * @param extras The {@link Extras} for the worker 398 * @return The instance of {@link Worker} 399 * 400 * @hide 401 */ 402 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 403 @SuppressWarnings("ClassNewInstance") workerFromClassName( @onNull Context context, @NonNull String workerClassName, @NonNull UUID workSpecId, @NonNull Extras extras)404 public static Worker workerFromClassName( 405 @NonNull Context context, 406 @NonNull String workerClassName, 407 @NonNull UUID workSpecId, 408 @NonNull Extras extras) { 409 Context appContext = context.getApplicationContext(); 410 try { 411 Class<?> clazz = Class.forName(workerClassName); 412 Worker worker = (Worker) clazz.newInstance(); 413 Method internalInitMethod = Worker.class.getDeclaredMethod( 414 "internalInit", 415 Context.class, 416 UUID.class, 417 Extras.class); 418 internalInitMethod.setAccessible(true); 419 internalInitMethod.invoke( 420 worker, 421 appContext, 422 workSpecId, 423 extras); 424 return worker; 425 } catch (Exception e) { 426 Log.e(TAG, "Trouble instantiating " + workerClassName, e); 427 } 428 return null; 429 } 430 431 /** 432 * Builder class for {@link WorkerWrapper} 433 * @hide 434 */ 435 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 436 public static class Builder { 437 private Context mAppContext; 438 @Nullable 439 private Worker mWorker; 440 private Configuration mConfiguration; 441 private WorkDatabase mWorkDatabase; 442 private String mWorkSpecId; 443 private ExecutionListener mListener; 444 private List<Scheduler> mSchedulers; 445 private Extras.RuntimeExtras mRuntimeExtras; 446 Builder(@onNull Context context, @NonNull Configuration configuration, @NonNull WorkDatabase database, @NonNull String workSpecId)447 public Builder(@NonNull Context context, 448 @NonNull Configuration configuration, 449 @NonNull WorkDatabase database, 450 @NonNull String workSpecId) { 451 mAppContext = context.getApplicationContext(); 452 mConfiguration = configuration; 453 mWorkDatabase = database; 454 mWorkSpecId = workSpecId; 455 } 456 457 /** 458 * @param listener The {@link ExecutionListener} which gets notified on completion of the 459 * {@link Worker} with the given {@code workSpecId}. 460 * @return The instance of {@link Builder} for chaining. 461 */ withListener(ExecutionListener listener)462 public Builder withListener(ExecutionListener listener) { 463 mListener = listener; 464 return this; 465 } 466 467 /** 468 * @param schedulers The list of {@link Scheduler}s used for scheduling {@link Worker}s. 469 * @return The instance of {@link Builder} for chaining. 470 */ withSchedulers(List<Scheduler> schedulers)471 public Builder withSchedulers(List<Scheduler> schedulers) { 472 mSchedulers = schedulers; 473 return this; 474 } 475 476 /** 477 * @param runtimeExtras The {@link Extras.RuntimeExtras} for the {@link Worker}. 478 * @return The instance of {@link Builder} for chaining. 479 */ withRuntimeExtras(Extras.RuntimeExtras runtimeExtras)480 public Builder withRuntimeExtras(Extras.RuntimeExtras runtimeExtras) { 481 mRuntimeExtras = runtimeExtras; 482 return this; 483 } 484 485 /** 486 * @param worker The instance of {@link Worker} to be executed by {@link WorkerWrapper}. 487 * Useful in the context of testing. 488 * @return The instance of {@link Builder} for chaining. 489 */ 490 @VisibleForTesting withWorker(Worker worker)491 public Builder withWorker(Worker worker) { 492 mWorker = worker; 493 return this; 494 } 495 496 /** 497 * @return The instance of {@link WorkerWrapper}. 498 */ build()499 public WorkerWrapper build() { 500 return new WorkerWrapper(this); 501 } 502 } 503 } 504