/* * Copyright 2017 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 androidx.work.impl; import static androidx.work.State.CANCELLED; import static androidx.work.State.ENQUEUED; import static androidx.work.State.FAILED; import static androidx.work.State.RUNNING; import static androidx.work.State.SUCCEEDED; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; import androidx.work.Configuration; import androidx.work.Data; import androidx.work.InputMerger; import androidx.work.State; import androidx.work.Worker; import androidx.work.impl.model.DependencyDao; import androidx.work.impl.model.WorkSpec; import androidx.work.impl.model.WorkSpecDao; import androidx.work.impl.model.WorkTagDao; import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * A runnable that looks up the {@link WorkSpec} from the database for a given id, instantiates * its Worker, and then calls it. * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class WorkerWrapper implements Runnable { private static final String TAG = "WorkerWrapper"; private Context mAppContext; private String mWorkSpecId; private ExecutionListener mListener; private List mSchedulers; private Extras.RuntimeExtras mRuntimeExtras; private WorkSpec mWorkSpec; Worker mWorker; private Configuration mConfiguration; private WorkDatabase mWorkDatabase; private WorkSpecDao mWorkSpecDao; private DependencyDao mDependencyDao; private WorkTagDao mWorkTagDao; private volatile boolean mInterrupted; private WorkerWrapper(Builder builder) { mAppContext = builder.mAppContext; mWorkSpecId = builder.mWorkSpecId; mListener = builder.mListener; mSchedulers = builder.mSchedulers; mRuntimeExtras = builder.mRuntimeExtras; mWorker = builder.mWorker; mConfiguration = builder.mConfiguration; mWorkDatabase = builder.mWorkDatabase; mWorkSpecDao = mWorkDatabase.workSpecDao(); mDependencyDao = mWorkDatabase.dependencyDao(); mWorkTagDao = mWorkDatabase.workTagDao(); } @WorkerThread @Override public void run() { if (tryCheckForInterruptionAndNotify()) { return; } mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId); if (mWorkSpec == null) { Log.e(TAG, String.format("Didn't find WorkSpec for id %s", mWorkSpecId)); notifyListener(false, false); return; } // Do a quick check to make sure we don't need to bail out in case this work is already // running, finished, or is blocked. if (mWorkSpec.state != ENQUEUED) { notifyIncorrectStatus(); return; } // Merge inputs. This can be potentially expensive code, so this should not be done inside // a database transaction. Data input; if (mWorkSpec.isPeriodic()) { input = mWorkSpec.input; } else { InputMerger inputMerger = InputMerger.fromClassName(mWorkSpec.inputMergerClassName); if (inputMerger == null) { Log.e(TAG, String.format("Could not create Input Merger %s", mWorkSpec.inputMergerClassName)); setFailedAndNotify(); return; } List inputs = new ArrayList<>(); inputs.add(mWorkSpec.input); inputs.addAll(mWorkSpecDao.getInputsFromPrerequisites(mWorkSpecId)); input = inputMerger.merge(inputs); } Extras extras = new Extras( input, mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId), mRuntimeExtras, mWorkSpec.runAttemptCount); // Not always creating a worker here, as the WorkerWrapper.Builder can set a worker override // in test mode. if (mWorker == null) { mWorker = workerFromWorkSpec(mAppContext, mWorkSpec, extras); } if (mWorker == null) { Log.e(TAG, String.format("Could for create Worker %s", mWorkSpec.workerClassName)); setFailedAndNotify(); return; } // Try to set the work to the running state. Note that this may fail because another thread // may have modified the DB since we checked last at the top of this function. if (trySetRunning()) { if (tryCheckForInterruptionAndNotify()) { return; } Worker.Result result; try { result = mWorker.doWork(); } catch (Exception | Error e) { result = Worker.Result.FAILURE; } try { mWorkDatabase.beginTransaction(); if (!tryCheckForInterruptionAndNotify()) { State state = mWorkSpecDao.getState(mWorkSpecId); if (state == null) { // state can be null here with a REPLACE on beginUniqueWork(). // Treat it as a failure, and rescheduleAndNotify() will // turn into a no-op. We still need to notify potential observers // holding on to wake locks on our behalf. notifyListener(false, false); } else if (state == RUNNING) { handleResult(result); } else if (!state.isFinished()) { rescheduleAndNotify(); } mWorkDatabase.setTransactionSuccessful(); } } finally { mWorkDatabase.endTransaction(); } } else { notifyIncorrectStatus(); } } /** * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public void interrupt(boolean cancelled) { mInterrupted = true; // Worker can be null if run() hasn't been called yet. if (mWorker != null) { mWorker.stop(cancelled); } } private void notifyIncorrectStatus() { State status = mWorkSpecDao.getState(mWorkSpecId); if (status == RUNNING) { Log.d(TAG, String.format("Status for %s is RUNNING;" + "not doing any work and rescheduling for later execution", mWorkSpecId)); notifyListener(false, true); } else { Log.e(TAG, String.format("Status for %s is %s; not doing any work", mWorkSpecId, status)); notifyListener(false, false); } } private boolean tryCheckForInterruptionAndNotify() { if (mInterrupted) { Log.d(TAG, String.format("Work interrupted for %s", mWorkSpecId)); State currentState = mWorkSpecDao.getState(mWorkSpecId); if (currentState == null) { // This can happen because of a beginUniqueWork(..., REPLACE, ...). Notify the // listeners so we can clean up any wake locks, etc. notifyListener(false, false); } else { notifyListener(currentState == SUCCEEDED, !currentState.isFinished()); } return true; } return false; } private void notifyListener(final boolean isSuccessful, final boolean needsReschedule) { if (mListener == null) { return; } WorkManagerTaskExecutor.getInstance().postToMainThread(new Runnable() { @Override public void run() { mListener.onExecuted(mWorkSpecId, isSuccessful, needsReschedule); } }); } private void handleResult(Worker.Result result) { switch (result) { case SUCCESS: { Log.d(TAG, String.format("Worker result SUCCESS for %s", mWorkSpecId)); if (mWorkSpec.isPeriodic()) { resetPeriodicAndNotify(true); } else { setSucceededAndNotify(); } break; } case RETRY: { Log.d(TAG, String.format("Worker result RETRY for %s", mWorkSpecId)); rescheduleAndNotify(); break; } case FAILURE: default: { Log.d(TAG, String.format("Worker result FAILURE for %s", mWorkSpecId)); if (mWorkSpec.isPeriodic()) { resetPeriodicAndNotify(false); } else { setFailedAndNotify(); } } } } private boolean trySetRunning() { boolean setToRunning = false; mWorkDatabase.beginTransaction(); try { State currentState = mWorkSpecDao.getState(mWorkSpecId); if (currentState == ENQUEUED) { mWorkSpecDao.setState(RUNNING, mWorkSpecId); mWorkSpecDao.incrementWorkSpecRunAttemptCount(mWorkSpecId); mWorkDatabase.setTransactionSuccessful(); setToRunning = true; } } finally { mWorkDatabase.endTransaction(); } return setToRunning; } private void setFailedAndNotify() { mWorkDatabase.beginTransaction(); try { recursivelyFailWorkAndDependents(mWorkSpecId); // Try to set the output for the failed work but check if the worker exists; this could // be a permanent error where we couldn't find or create the worker class. if (mWorker != null) { // Update Data as necessary. Data output = mWorker.getOutputData(); mWorkSpecDao.setOutput(mWorkSpecId, output); } mWorkDatabase.setTransactionSuccessful(); } finally { mWorkDatabase.endTransaction(); notifyListener(false, false); } Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers); } private void recursivelyFailWorkAndDependents(String workSpecId) { List dependentIds = mDependencyDao.getDependentWorkIds(workSpecId); for (String id : dependentIds) { recursivelyFailWorkAndDependents(id); } // Don't fail already cancelled work. if (mWorkSpecDao.getState(workSpecId) != CANCELLED) { mWorkSpecDao.setState(FAILED, workSpecId); } } private void rescheduleAndNotify() { mWorkDatabase.beginTransaction(); try { mWorkSpecDao.setState(ENQUEUED, mWorkSpecId); // TODO(xbhatnag): Period Start Time is confusing for non-periodic work. Rename. mWorkSpecDao.setPeriodStartTime(mWorkSpecId, System.currentTimeMillis()); mWorkDatabase.setTransactionSuccessful(); } finally { mWorkDatabase.endTransaction(); notifyListener(false, true); } } private void resetPeriodicAndNotify(boolean isSuccessful) { mWorkDatabase.beginTransaction(); try { long currentPeriodStartTime = mWorkSpec.periodStartTime; long nextPeriodStartTime = currentPeriodStartTime + mWorkSpec.intervalDuration; mWorkSpecDao.setPeriodStartTime(mWorkSpecId, nextPeriodStartTime); mWorkSpecDao.setState(ENQUEUED, mWorkSpecId); mWorkSpecDao.resetWorkSpecRunAttemptCount(mWorkSpecId); mWorkDatabase.setTransactionSuccessful(); } finally { mWorkDatabase.endTransaction(); notifyListener(isSuccessful, false); } } private void setSucceededAndNotify() { mWorkDatabase.beginTransaction(); try { mWorkSpecDao.setState(SUCCEEDED, mWorkSpecId); // Update Data as necessary. Data output = mWorker.getOutputData(); mWorkSpecDao.setOutput(mWorkSpecId, output); // Unblock Dependencies and set Period Start Time long currentTimeMillis = System.currentTimeMillis(); List dependentWorkIds = mDependencyDao.getDependentWorkIds(mWorkSpecId); for (String dependentWorkId : dependentWorkIds) { if (mDependencyDao.hasCompletedAllPrerequisites(dependentWorkId)) { Log.d(TAG, String.format("Setting status to enqueued for %s", dependentWorkId)); mWorkSpecDao.setState(ENQUEUED, dependentWorkId); mWorkSpecDao.setPeriodStartTime(dependentWorkId, currentTimeMillis); } } mWorkDatabase.setTransactionSuccessful(); } finally { mWorkDatabase.endTransaction(); notifyListener(true, false); } // This takes of scheduling the dependent workers as they have been marked ENQUEUED. Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers); } static Worker workerFromWorkSpec(@NonNull Context context, @NonNull WorkSpec workSpec, @NonNull Extras extras) { String workerClassName = workSpec.workerClassName; UUID workSpecId = UUID.fromString(workSpec.id); return workerFromClassName( context, workerClassName, workSpecId, extras); } /** * Creates a {@link Worker} reflectively & initializes the worker. * * @param context The application {@link Context} * @param workerClassName The fully qualified class name for the {@link Worker} * @param workSpecId The {@link WorkSpec} identifier * @param extras The {@link Extras} for the worker * @return The instance of {@link Worker} * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @SuppressWarnings("ClassNewInstance") public static Worker workerFromClassName( @NonNull Context context, @NonNull String workerClassName, @NonNull UUID workSpecId, @NonNull Extras extras) { Context appContext = context.getApplicationContext(); try { Class clazz = Class.forName(workerClassName); Worker worker = (Worker) clazz.newInstance(); Method internalInitMethod = Worker.class.getDeclaredMethod( "internalInit", Context.class, UUID.class, Extras.class); internalInitMethod.setAccessible(true); internalInitMethod.invoke( worker, appContext, workSpecId, extras); return worker; } catch (Exception e) { Log.e(TAG, "Trouble instantiating " + workerClassName, e); } return null; } /** * Builder class for {@link WorkerWrapper} * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public static class Builder { private Context mAppContext; @Nullable private Worker mWorker; private Configuration mConfiguration; private WorkDatabase mWorkDatabase; private String mWorkSpecId; private ExecutionListener mListener; private List mSchedulers; private Extras.RuntimeExtras mRuntimeExtras; public Builder(@NonNull Context context, @NonNull Configuration configuration, @NonNull WorkDatabase database, @NonNull String workSpecId) { mAppContext = context.getApplicationContext(); mConfiguration = configuration; mWorkDatabase = database; mWorkSpecId = workSpecId; } /** * @param listener The {@link ExecutionListener} which gets notified on completion of the * {@link Worker} with the given {@code workSpecId}. * @return The instance of {@link Builder} for chaining. */ public Builder withListener(ExecutionListener listener) { mListener = listener; return this; } /** * @param schedulers The list of {@link Scheduler}s used for scheduling {@link Worker}s. * @return The instance of {@link Builder} for chaining. */ public Builder withSchedulers(List schedulers) { mSchedulers = schedulers; return this; } /** * @param runtimeExtras The {@link Extras.RuntimeExtras} for the {@link Worker}. * @return The instance of {@link Builder} for chaining. */ public Builder withRuntimeExtras(Extras.RuntimeExtras runtimeExtras) { mRuntimeExtras = runtimeExtras; return this; } /** * @param worker The instance of {@link Worker} to be executed by {@link WorkerWrapper}. * Useful in the context of testing. * @return The instance of {@link Builder} for chaining. */ @VisibleForTesting public Builder withWorker(Worker worker) { mWorker = worker; return this; } /** * @return The instance of {@link WorkerWrapper}. */ public WorkerWrapper build() { return new WorkerWrapper(this); } } }