[go: nahoru, domu]

blob: eee1b9c0f051df1ef46bc1499c18be8172fddf15 [file] [log] [blame]
/*
* 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.WorkInfo.State.BLOCKED;
import static androidx.work.WorkInfo.State.CANCELLED;
import static androidx.work.WorkInfo.State.ENQUEUED;
import static androidx.work.WorkInfo.State.FAILED;
import static androidx.work.WorkInfo.State.RUNNING;
import static androidx.work.WorkInfo.State.SUCCEEDED;
import static androidx.work.impl.model.WorkSpec.SCHEDULE_NOT_REQUESTED_YET;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.work.Configuration;
import androidx.work.Data;
import androidx.work.InputMerger;
import androidx.work.InputMergerFactory;
import androidx.work.ListenableWorker;
import androidx.work.Logger;
import androidx.work.WorkInfo;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.impl.background.systemalarm.RescheduleReceiver;
import androidx.work.impl.foreground.ForegroundProcessor;
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.PackageManagerHelper;
import androidx.work.impl.utils.WorkForegroundRunnable;
import androidx.work.impl.utils.WorkForegroundUpdater;
import androidx.work.impl.utils.WorkProgressUpdater;
import androidx.work.impl.utils.futures.SettableFuture;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
/**
* 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 {
// Avoid Synthetic accessor
static final String TAG = Logger.tagWithPrefix("WorkerWrapper");
// Avoid Synthetic accessor
Context mAppContext;
private String mWorkSpecId;
private List<Scheduler> mSchedulers;
private WorkerParameters.RuntimeExtras mRuntimeExtras;
// Avoid Synthetic accessor
WorkSpec mWorkSpec;
ListenableWorker mWorker;
TaskExecutor mWorkTaskExecutor;
// Package-private for synthetic accessor.
@NonNull
ListenableWorker.Result mResult = ListenableWorker.Result.failure();
private Configuration mConfiguration;
private ForegroundProcessor mForegroundProcessor;
private WorkDatabase mWorkDatabase;
private WorkSpecDao mWorkSpecDao;
private DependencyDao mDependencyDao;
private WorkTagDao mWorkTagDao;
private List<String> mTags;
private String mWorkDescription;
// Synthetic access
@NonNull
SettableFuture<Boolean> mFuture = SettableFuture.create();
// Package-private for synthetic accessor.
@Nullable ListenableFuture<ListenableWorker.Result> mInnerFuture = null;
private volatile boolean mInterrupted;
// Package-private for synthetic accessor.
WorkerWrapper(@NonNull Builder builder) {
mAppContext = builder.mAppContext;
mWorkTaskExecutor = builder.mWorkTaskExecutor;
mForegroundProcessor = builder.mForegroundProcessor;
mWorkSpecId = builder.mWorkSpecId;
mSchedulers = builder.mSchedulers;
mRuntimeExtras = builder.mRuntimeExtras;
mWorker = builder.mWorker;
mConfiguration = builder.mConfiguration;
mWorkDatabase = builder.mWorkDatabase;
mWorkSpecDao = mWorkDatabase.workSpecDao();
mDependencyDao = mWorkDatabase.dependencyDao();
mWorkTagDao = mWorkDatabase.workTagDao();
}
public @NonNull ListenableFuture<Boolean> getFuture() {
return mFuture;
}
@WorkerThread
@Override
public void run() {
mTags = mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId);
mWorkDescription = createWorkDescription(mTags);
runWorker();
}
private void runWorker() {
if (tryCheckForInterruptionAndResolve()) {
return;
}
mWorkDatabase.beginTransaction();
try {
mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
if (mWorkSpec == null) {
Logger.get().error(
TAG,
"Didn't find WorkSpec for id " + mWorkSpecId);
resolve(false);
mWorkDatabase.setTransactionSuccessful();
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) {
resolveIncorrectStatus();
mWorkDatabase.setTransactionSuccessful();
Logger.get().debug(TAG,
mWorkSpec.workerClassName
+ " is not in ENQUEUED state. Nothing more to do");
return;
}
// Case 1:
// Ensure that Workers that are backed off are only executed when they are supposed to.
// GreedyScheduler can schedule WorkSpecs that have already been backed off because
// it is holding on to snapshots of WorkSpecs. So WorkerWrapper needs to determine
// if the ListenableWorker is actually eligible to execute at this point in time.
// Case 2:
// On API 23, we double scheduler Workers because JobScheduler prefers batching.
// So is the Work is periodic, we only need to execute it once per interval.
// Also potential bugs in the platform may cause a Job to run more than once.
if (mWorkSpec.isPeriodic() || mWorkSpec.isBackedOff()) {
long now = System.currentTimeMillis();
// Allow first run of a PeriodicWorkRequest
// to go through. This is because when periodStartTime=0;
// calculateNextRunTime() always > now.
// For more information refer to b/124274584
boolean isFirstRun = mWorkSpec.periodStartTime == 0;
if (!isFirstRun && now < mWorkSpec.calculateNextRunTime()) {
Logger.get().debug(TAG,
String.format(
"Delaying execution for %s because it is being executed "
+ "before schedule.",
mWorkSpec.workerClassName));
// For AlarmManager implementation we need to reschedule this kind of Work.
// This is not a problem for JobScheduler because we will only reschedule
// work if JobScheduler is unaware of a jobId.
resolve(true);
mWorkDatabase.setTransactionSuccessful();
return;
}
}
// Needed for nested transactions, such as when we're in a dependent work request when
// using a SynchronousExecutor.
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
}
// 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 {
InputMergerFactory inputMergerFactory = mConfiguration.getInputMergerFactory();
String inputMergerClassName = mWorkSpec.inputMergerClassName;
InputMerger inputMerger =
inputMergerFactory.createInputMergerWithDefaultFallback(inputMergerClassName);
if (inputMerger == null) {
Logger.get().error(TAG, "Could not create Input Merger " + mWorkSpec.inputMergerClassName);
setFailedAndResolve();
return;
}
List<Data> inputs = new ArrayList<>();
inputs.add(mWorkSpec.input);
inputs.addAll(mWorkSpecDao.getInputsFromPrerequisites(mWorkSpecId));
input = inputMerger.merge(inputs);
}
final WorkerParameters params = new WorkerParameters(
UUID.fromString(mWorkSpecId),
input,
mTags,
mRuntimeExtras,
mWorkSpec.runAttemptCount,
mConfiguration.getExecutor(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
new WorkProgressUpdater(mWorkDatabase, mWorkTaskExecutor),
new WorkForegroundUpdater(mWorkDatabase, mForegroundProcessor, mWorkTaskExecutor));
// Not always creating a worker here, as the WorkerWrapper.Builder can set a worker override
// in test mode.
if (mWorker == null) {
mWorker = mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
mAppContext,
mWorkSpec.workerClassName,
params);
}
if (mWorker == null) {
Logger.get().error(TAG,
"Could not create Worker " + mWorkSpec.workerClassName);
setFailedAndResolve();
return;
}
if (mWorker.isUsed()) {
Logger.get().error(TAG, "Received an already-used Worker " + mWorkSpec.workerClassName
+ "; Worker Factory should return new instances");
setFailedAndResolve();
return;
}
mWorker.setUsed();
// 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 (tryCheckForInterruptionAndResolve()) {
return;
}
final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
final WorkForegroundRunnable foregroundRunnable =
new WorkForegroundRunnable(
mAppContext,
mWorkSpec,
mWorker,
params.getForegroundUpdater(),
mWorkTaskExecutor
);
mWorkTaskExecutor.getMainThreadExecutor().execute(foregroundRunnable);
final ListenableFuture<Void> runExpedited = foregroundRunnable.getFuture();
runExpedited.addListener(new Runnable() {
@Override
public void run() {
try {
runExpedited.get();
Logger.get().debug(TAG,
"Starting work for " + mWorkSpec.workerClassName);
// Call mWorker.startWork() on the main thread.
mInnerFuture = mWorker.startWork();
future.setFuture(mInnerFuture);
} catch (Throwable e) {
future.setException(e);
}
}
}, mWorkTaskExecutor.getMainThreadExecutor());
// Avoid synthetic accessors.
final String workDescription = mWorkDescription;
future.addListener(new Runnable() {
@Override
@SuppressLint("SyntheticAccessor")
public void run() {
try {
// If the ListenableWorker returns a null result treat it as a failure.
ListenableWorker.Result result = future.get();
if (result == null) {
Logger.get().error(TAG, mWorkSpec.workerClassName
+ " returned a null result. Treating it as a failure.");
} else {
Logger.get().debug(TAG,
mWorkSpec.workerClassName + " returned a " + result + ".");
mResult = result;
}
} catch (CancellationException exception) {
// Cancellations need to be treated with care here because innerFuture
// cancellations will bubble up, and we need to gracefully handle that.
Logger.get().info(TAG, workDescription + " was cancelled", exception);
} catch (InterruptedException | ExecutionException exception) {
Logger.get().error(TAG,
workDescription + " failed because it threw an exception/error");
} finally {
onWorkFinished();
}
}
}, mWorkTaskExecutor.getBackgroundExecutor());
} else {
resolveIncorrectStatus();
}
}
// Package-private for synthetic accessor.
void onWorkFinished() {
if (!tryCheckForInterruptionAndResolve()) {
mWorkDatabase.beginTransaction();
try {
WorkInfo.State state = mWorkSpecDao.getState(mWorkSpecId);
mWorkDatabase.workProgressDao().delete(mWorkSpecId);
if (state == null) {
// state can be null here with a REPLACE on beginUniqueWork().
// Treat it as a failure, and rescheduleAndResolve() will
// turn into a no-op. We still need to notify potential observers
// holding on to wake locks on our behalf.
resolve(false);
} else if (state == RUNNING) {
handleResult(mResult);
} else if (!state.isFinished()) {
rescheduleAndResolve();
}
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
}
}
// Try to schedule any newly-unblocked workers, and workers requiring rescheduling (such as
// periodic work using AlarmManager). This code runs after runWorker() because it should
// happen in its own transaction.
// Cancel this work in other schedulers. For example, if this work was
// handled by GreedyScheduler, we should make sure JobScheduler is informed
// that it should remove this job and AlarmManager should remove all related alarms.
if (mSchedulers != null) {
for (Scheduler scheduler : mSchedulers) {
scheduler.cancel(mWorkSpecId);
}
Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers);
}
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void interrupt() {
mInterrupted = true;
// Resolve WorkerWrapper's future so we do the right thing and setup a reschedule
// if necessary. mInterrupted is always true here, we don't really care about the return
// value.
tryCheckForInterruptionAndResolve();
boolean isDone = false;
if (mInnerFuture != null) {
// Propagate the cancellations to the inner future.
isDone = mInnerFuture.isDone();
mInnerFuture.cancel(true);
}
// Worker can be null if run() hasn't been called yet
if (mWorker != null && !isDone) {
mWorker.stop();
} else {
String message = "WorkSpec " + mWorkSpec + " is already done. Not interrupting.";
Logger.get().debug(TAG, message);
}
}
private void resolveIncorrectStatus() {
WorkInfo.State status = mWorkSpecDao.getState(mWorkSpecId);
if (status == RUNNING) {
Logger.get().debug(TAG, "Status for " + mWorkSpecId
+ " is RUNNING; not doing any work and rescheduling for later execution");
resolve(true);
} else {
Logger.get().debug(TAG,
"Status for " + mWorkSpecId + " is " + status + " ; not doing any work");
resolve(false);
}
}
private boolean tryCheckForInterruptionAndResolve() {
// Interruptions can happen when:
// An explicit cancel* signal
// A change in constraint, which causes WorkManager to stop the Worker.
// Worker exceeding a 10 min execution window.
// One scheduler completing a Worker, and telling other Schedulers to cleanup.
if (mInterrupted) {
Logger.get().debug(TAG, "Work interrupted for " + mWorkDescription);
WorkInfo.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.
resolve(false);
} else {
resolve(!currentState.isFinished());
}
return true;
}
return false;
}
private void resolve(final boolean needsReschedule) {
mWorkDatabase.beginTransaction();
try {
// IMPORTANT: We are using a transaction here as to ensure that we have some guarantees
// about the state of the world before we disable RescheduleReceiver.
// Check to see if there is more work to be done. If there is no more work, then
// disable RescheduleReceiver. Using a transaction here, as there could be more than
// one thread looking at the list of eligible WorkSpecs.
boolean hasUnfinishedWork = mWorkDatabase.workSpecDao().hasUnfinishedWork();
if (!hasUnfinishedWork) {
PackageManagerHelper.setComponentEnabled(
mAppContext, RescheduleReceiver.class, false);
}
if (needsReschedule) {
// Set state to ENQUEUED again.
// Reset scheduled state so its picked up by background schedulers again.
mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
mWorkSpecDao.markWorkSpecScheduled(mWorkSpecId, SCHEDULE_NOT_REQUESTED_YET);
}
if (mWorkSpec != null && mWorker != null && mWorker.isRunInForeground()) {
mForegroundProcessor.stopForeground(mWorkSpecId);
}
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
}
mFuture.set(needsReschedule);
}
private void handleResult(ListenableWorker.Result result) {
if (result instanceof ListenableWorker.Result.Success) {
Logger.get().info(
TAG,
"Worker result SUCCESS for " + mWorkDescription);
if (mWorkSpec.isPeriodic()) {
resetPeriodicAndResolve();
} else {
setSucceededAndResolve();
}
} else if (result instanceof ListenableWorker.Result.Retry) {
Logger.get().info(
TAG,
"Worker result RETRY for " + mWorkDescription);
rescheduleAndResolve();
} else {
Logger.get().info(
TAG,
"Worker result FAILURE for " + mWorkDescription);
if (mWorkSpec.isPeriodic()) {
resetPeriodicAndResolve();
} else {
setFailedAndResolve();
}
}
}
private boolean trySetRunning() {
boolean setToRunning = false;
mWorkDatabase.beginTransaction();
try {
WorkInfo.State currentState = mWorkSpecDao.getState(mWorkSpecId);
if (currentState == ENQUEUED) {
mWorkSpecDao.setState(RUNNING, mWorkSpecId);
mWorkSpecDao.incrementWorkSpecRunAttemptCount(mWorkSpecId);
setToRunning = true;
}
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
}
return setToRunning;
}
@VisibleForTesting
void setFailedAndResolve() {
mWorkDatabase.beginTransaction();
try {
iterativelyFailWorkAndDependents(mWorkSpecId);
ListenableWorker.Result.Failure failure = (ListenableWorker.Result.Failure) mResult;
// Update Data as necessary.
Data output = failure.getOutputData();
mWorkSpecDao.setOutput(mWorkSpecId, output);
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
resolve(false);
}
}
private void iterativelyFailWorkAndDependents(String workSpecId) {
@SuppressWarnings("JdkObsolete") // TODO(b/141962522): Suppressed during upgrade to AGP 3.6.
LinkedList<String> idsToProcess = new LinkedList<>();
idsToProcess.add(workSpecId);
while (!idsToProcess.isEmpty()) {
String id = idsToProcess.remove();
// Don't fail already cancelled work.
if (mWorkSpecDao.getState(id) != CANCELLED) {
mWorkSpecDao.setState(FAILED, id);
}
idsToProcess.addAll(mDependencyDao.getDependentWorkIds(id));
}
}
private void rescheduleAndResolve() {
mWorkDatabase.beginTransaction();
try {
mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
mWorkSpecDao.setPeriodStartTime(mWorkSpecId, System.currentTimeMillis());
mWorkSpecDao.markWorkSpecScheduled(mWorkSpecId, SCHEDULE_NOT_REQUESTED_YET);
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
resolve(true);
}
}
private void resetPeriodicAndResolve() {
mWorkDatabase.beginTransaction();
try {
// The system clock may have been changed such that the periodStartTime was in the past.
// Therefore we always use the current time to determine the next run time of a Worker.
// This way, the Schedulers will correctly schedule the next instance of the
// PeriodicWork in the future. This happens in calculateNextRunTime() in WorkSpec.
mWorkSpecDao.setPeriodStartTime(mWorkSpecId, System.currentTimeMillis());
mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
mWorkSpecDao.resetWorkSpecRunAttemptCount(mWorkSpecId);
mWorkSpecDao.markWorkSpecScheduled(mWorkSpecId, SCHEDULE_NOT_REQUESTED_YET);
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
resolve(false);
}
}
private void setSucceededAndResolve() {
mWorkDatabase.beginTransaction();
try {
mWorkSpecDao.setState(SUCCEEDED, mWorkSpecId);
ListenableWorker.Result.Success success = (ListenableWorker.Result.Success) mResult;
// Update Data as necessary.
Data output = success.getOutputData();
mWorkSpecDao.setOutput(mWorkSpecId, output);
// Unblock Dependencies and set Period Start Time
long currentTimeMillis = System.currentTimeMillis();
List<String> dependentWorkIds = mDependencyDao.getDependentWorkIds(mWorkSpecId);
for (String dependentWorkId : dependentWorkIds) {
if (mWorkSpecDao.getState(dependentWorkId) == BLOCKED
&& mDependencyDao.hasCompletedAllPrerequisites(dependentWorkId)) {
Logger.get().info(TAG,
"Setting status to enqueued for " + dependentWorkId);
mWorkSpecDao.setState(ENQUEUED, dependentWorkId);
mWorkSpecDao.setPeriodStartTime(dependentWorkId, currentTimeMillis);
}
}
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
resolve(false);
}
}
private String createWorkDescription(List<String> tags) {
StringBuilder sb = new StringBuilder("Work [ id=")
.append(mWorkSpecId)
.append(", tags={ ");
boolean first = true;
for (String tag : tags) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(tag);
}
sb.append(" } ]");
return sb.toString();
}
/**
* Builder class for {@link WorkerWrapper}
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static class Builder {
@NonNull Context mAppContext;
@Nullable
ListenableWorker mWorker;
@NonNull ForegroundProcessor mForegroundProcessor;
@NonNull TaskExecutor mWorkTaskExecutor;
@NonNull Configuration mConfiguration;
@NonNull WorkDatabase mWorkDatabase;
@NonNull String mWorkSpecId;
List<Scheduler> mSchedulers;
@NonNull
WorkerParameters.RuntimeExtras mRuntimeExtras = new WorkerParameters.RuntimeExtras();
public Builder(@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
@NonNull ForegroundProcessor foregroundProcessor,
@NonNull WorkDatabase database,
@NonNull String workSpecId) {
mAppContext = context.getApplicationContext();
mWorkTaskExecutor = workTaskExecutor;
mForegroundProcessor = foregroundProcessor;
mConfiguration = configuration;
mWorkDatabase = database;
mWorkSpecId = workSpecId;
}
/**
* @param schedulers The list of {@link Scheduler}s used for scheduling {@link Worker}s.
* @return The instance of {@link Builder} for chaining.
*/
@NonNull
public Builder withSchedulers(@NonNull List<Scheduler> schedulers) {
mSchedulers = schedulers;
return this;
}
/**
* @param runtimeExtras The {@link WorkerParameters.RuntimeExtras} for the {@link Worker};
* if this is {@code null}, it will be ignored and the default value
* will be retained.
* @return The instance of {@link Builder} for chaining.
*/
@NonNull
public Builder withRuntimeExtras(@Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
if (runtimeExtras != null) {
mRuntimeExtras = runtimeExtras;
}
return this;
}
/**
* @param worker The instance of {@link ListenableWorker} to be executed by
* {@link WorkerWrapper}. Useful in the context of testing.
* @return The instance of {@link Builder} for chaining.
*/
@NonNull
@VisibleForTesting
public Builder withWorker(@NonNull ListenableWorker worker) {
mWorker = worker;
return this;
}
/**
* @return The instance of {@link WorkerWrapper}.
*/
@NonNull
public WorkerWrapper build() {
return new WorkerWrapper(this);
}
}
}