blob: 522937ca990d87a05f9f9449c2873d0364e95db3 [file] [log] [blame]
/*
* Copyright 2018 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.workers;
import static androidx.work.ListenableWorker.Result.FAILURE;
import static androidx.work.ListenableWorker.Result.RETRY;
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.text.TextUtils;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import androidx.work.Logger;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.constraints.WorkConstraintsCallback;
import androidx.work.impl.constraints.WorkConstraintsTracker;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.futures.SettableFuture;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.List;
/**
* Is an implementation of a {@link Worker} that can delegate to a different {@link Worker}
* when the constraints are met.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class ConstraintTrackingWorker extends ListenableWorker implements WorkConstraintsCallback {
private static final String TAG = "ConstraintTrkngWrkr";
/**
* The {@code className} of the {@link Worker} to delegate to.
*/
public static final String ARGUMENT_CLASS_NAME =
"androidx.work.impl.workers.ConstraintTrackingWorker.ARGUMENT_CLASS_NAME";
private WorkerParameters mWorkerParameters;
// These are package-private to avoid synthetic accessor.
final Object mLock;
// Marking this volatile as the delegated workers could switch threads.
volatile boolean mAreConstraintsUnmet;
SettableFuture<Payload> mFuture;
@Nullable private ListenableWorker mDelegate;
public ConstraintTrackingWorker(@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
mWorkerParameters = workerParams;
mLock = new Object();
mAreConstraintsUnmet = false;
mFuture = SettableFuture.create();
}
@NonNull
@Override
public ListenableFuture<Payload> startWork() {
getBackgroundExecutor().execute(new Runnable() {
@Override
public void run() {
setupAndRunConstraintTrackingWork();
}
});
return mFuture;
}
// Package-private to avoid synthetic accessor.
void setupAndRunConstraintTrackingWork() {
String className = getInputData().getString(ARGUMENT_CLASS_NAME);
if (TextUtils.isEmpty(className)) {
Logger.error(TAG, "No worker to delegate to.");
setFutureFailed();
return;
}
mDelegate = getWorkerFactory().createWorkerWithDefaultFallback(
getApplicationContext(),
className,
mWorkerParameters);
if (mDelegate == null) {
Logger.debug(TAG, "No worker to delegate to.");
setFutureFailed();
return;
}
WorkDatabase workDatabase = getWorkDatabase();
// We need to know what the real constraints are for the delegate.
WorkSpec workSpec = workDatabase.workSpecDao().getWorkSpec(getId().toString());
if (workSpec == null) {
setFutureFailed();
return;
}
WorkConstraintsTracker workConstraintsTracker =
new WorkConstraintsTracker(getApplicationContext(), this);
// Start tracking
workConstraintsTracker.replace(Collections.singletonList(workSpec));
if (workConstraintsTracker.areAllConstraintsMet(getId().toString())) {
Logger.debug(TAG, String.format("Constraints met for delegate %s", className));
// Wrapping the call to mDelegate#doWork() in a try catch, because
// changes in constraints can cause the worker to throw RuntimeExceptions, and
// that should cause a retry.
try {
final ListenableFuture<Payload> innerFuture = mDelegate.startWork();
innerFuture.addListener(new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (mAreConstraintsUnmet) {
setFutureRetry();
} else {
mFuture.setFuture(innerFuture);
}
}
}
}, getBackgroundExecutor());
} catch (Throwable exception) {
Logger.debug(TAG, String.format(
"Delegated worker %s threw exception in startWork.", className),
exception);
synchronized (mLock) {
if (mAreConstraintsUnmet) {
Logger.debug(TAG, "Constraints were unmet, Retrying.");
setFutureRetry();
} else {
setFutureFailed();
}
}
}
} else {
Logger.debug(TAG, String.format(
"Constraints not met for delegate %s. Requesting retry.", className));
setFutureRetry();
}
}
// Package-private to avoid synthetic accessor.
void setFutureFailed() {
mFuture.set(new Payload(FAILURE, Data.EMPTY));
}
// Package-private to avoid synthetic accessor.
void setFutureRetry() {
mFuture.set(new Payload(RETRY, Data.EMPTY));
}
@Override
public void onStopped(boolean cancelled) {
super.onStopped(cancelled);
if (mDelegate != null) {
// Stop is the method that sets the stopped and cancelled bits and invokes onStopped.
mDelegate.stop(cancelled);
}
}
/**
* @return The instance of {@link WorkDatabase}
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
public WorkDatabase getWorkDatabase() {
return WorkManagerImpl.getInstance().getWorkDatabase();
}
/**
* @return The {@link Worker} used for delegated work
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
public ListenableWorker getDelegate() {
return mDelegate;
}
@Override
public void onAllConstraintsMet(@NonNull List<String> workSpecIds) {
// WorkConstraintTracker notifies on the main thread. So we don't want to trampoline
// between the background thread and the main thread in this case.
}
@Override
public void onAllConstraintsNotMet(@NonNull List<String> workSpecIds) {
// If at any point, constraints are not met mark it so we can retry the work.
Logger.debug(TAG, String.format("Constraints changed for %s", workSpecIds));
synchronized (mLock) {
mAreConstraintsUnmet = true;
}
}
}