public class

ConstraintTrackingWorker

extends ListenableWorker

implements WorkConstraintsCallback

 java.lang.Object

androidx.work.ListenableWorker

↳androidx.work.impl.workers.ConstraintTrackingWorker

Overview

Is an implementation of a Worker that can delegate to a different Worker when the constraints are met.

Summary

Fields
public static final java.lang.StringARGUMENT_CLASS_NAME

The className of the Worker to delegate to.

Constructors
publicConstraintTrackingWorker(Context appContext, WorkerParameters workerParams)

Methods
public ListenableWorkergetDelegate()

public TaskExecutorgetTaskExecutor()

public TrackersgetTrackers()

public WorkDatabasegetWorkDatabase()

public voidonAllConstraintsMet(java.util.List<java.lang.String> workSpecIds)

public voidonAllConstraintsNotMet(java.util.List<java.lang.String> workSpecIds)

public voidonStopped()

This method is invoked when this Worker has been told to stop.

public abstract <any>startWork()

Override this method to start your actual background processing.

from ListenableWorkergetApplicationContext, getBackgroundExecutor, getForegroundInfoAsync, getId, getInputData, getNetwork, getRunAttemptCount, getStopReason, getTags, getTriggeredContentAuthorities, getTriggeredContentUris, getWorkerFactory, isStopped, isUsed, setForegroundAsync, setProgressAsync, setUsed, stop
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String ARGUMENT_CLASS_NAME

The className of the Worker to delegate to.

Constructors

public ConstraintTrackingWorker(Context appContext, WorkerParameters workerParams)

Methods

public abstract <any> startWork()

Override this method to start your actual background processing. This method is called on the main thread.

A ListenableWorker has a well defined execution window to to finish its execution and return a ListenableWorker.Result. After this time has expired, the worker will be signalled to stop and its com.google.common.util.concurrent.ListenableFuture will be cancelled.

The future will also be cancelled if this worker is stopped for any reason (see ListenableWorker.onStopped()).

Returns:

A com.google.common.util.concurrent.ListenableFuture with the ListenableWorker.Result of the computation. If you cancel this Future, WorkManager will treat this unit of work as failed.

public void onStopped()

This method is invoked when this Worker has been told to stop. At this point, the com.google.common.util.concurrent.ListenableFuture returned by the instance of ListenableWorker.startWork() is also cancelled. This could happen due to an explicit cancellation signal by the user, or because the system has decided to preempt the task. In these cases, the results of the work will be ignored by WorkManager. All processing in this method should be lightweight - there are no contractual guarantees about which thread will invoke this call, so this should not be a long-running or blocking operation.

public WorkDatabase getWorkDatabase()

Returns:

The instance of WorkDatabase

public TaskExecutor getTaskExecutor()

Returns:

The instance of TaskExecutor.

public Trackers getTrackers()

Returns:

The instance of Trackers.

public ListenableWorker getDelegate()

Returns:

The Worker used for delegated work

public void onAllConstraintsMet(java.util.List<java.lang.String> workSpecIds)

public void onAllConstraintsNotMet(java.util.List<java.lang.String> workSpecIds)

Source

/*
 * 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 android.content.Context;
import android.text.TextUtils;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
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.WorkConstraintsTrackerImpl;
import androidx.work.impl.constraints.trackers.Trackers;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.futures.SettableFuture;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;

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 = Logger.tagWithPrefix("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<Result> 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<Result> 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.get().error(TAG, "No worker to delegate to.");
            setFutureFailed();
            return;
        }

        mDelegate = getWorkerFactory().createWorkerWithDefaultFallback(
                getApplicationContext(),
                className,
                mWorkerParameters);

        if (mDelegate == null) {
            Logger.get().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;
        }
        WorkConstraintsTrackerImpl workConstraintsTracker =
                new WorkConstraintsTrackerImpl(getTrackers(), this);

        // Start tracking
        workConstraintsTracker.replace(Collections.singletonList(workSpec));

        if (workConstraintsTracker.areAllConstraintsMet(getId().toString())) {
            Logger.get().debug(TAG, "Constraints met for delegate " + 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<Result> 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.get().debug(TAG, String.format(
                        "Delegated worker %s threw exception in startWork.", className),
                        exception);
                synchronized (mLock) {
                    if (mAreConstraintsUnmet) {
                        Logger.get().debug(TAG, "Constraints were unmet, Retrying.");
                        setFutureRetry();
                    } else {
                        setFutureFailed();
                    }
                }
            }
        } else {
            Logger.get().debug(TAG, String.format(
                    "Constraints not met for delegate %s. Requesting retry.", className));
            setFutureRetry();
        }

    }

    // Package-private to avoid synthetic accessor.
    void setFutureFailed() {
        mFuture.set(Result.failure());
    }

    // Package-private to avoid synthetic accessor.
    void setFutureRetry() {
        mFuture.set(Result.retry());
    }

    @Override
    public void onStopped() {
        super.onStopped();
        if (mDelegate != null && !mDelegate.isStopped()) {
            // Stop is the method that sets the stopped and cancelled bits and invokes onStopped.
            mDelegate.stop();
        }
    }

    /**
     * @return The instance of {@link WorkDatabase}
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @VisibleForTesting
    @NonNull
    public WorkDatabase getWorkDatabase() {
        return WorkManagerImpl.getInstance(getApplicationContext()).getWorkDatabase();
    }

    /**
     * @return The instance of {@link TaskExecutor}.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @VisibleForTesting
    @NonNull
    @Override
    public TaskExecutor getTaskExecutor() {
        return WorkManagerImpl.getInstance(getApplicationContext()).getWorkTaskExecutor();
    }

    /**
     * @return The instance of {@link Trackers}.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @VisibleForTesting
    @NonNull
    public Trackers getTrackers() {
        return WorkManagerImpl.getInstance(getApplicationContext()).getTrackers();
    }

    /**
     * @return The {@link Worker} used for delegated work
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    @VisibleForTesting
    @Nullable
    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.get().debug(TAG, "Constraints changed for " + workSpecIds);
        synchronized (mLock) {
            mAreConstraintsUnmet = true;
        }
    }
}