Processor.java

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

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.work.Configuration;
import androidx.work.Logger;
import androidx.work.WorkerParameters;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;

/**
 * A Processor can intelligently schedule and execute work on demand.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class Processor implements ExecutionListener {
    private static final String TAG = Logger.tagWithPrefix("Processor");

    private Context mAppContext;
    private Configuration mConfiguration;
    private TaskExecutor mWorkTaskExecutor;
    private WorkDatabase mWorkDatabase;
    private Map<String, WorkerWrapper> mEnqueuedWorkMap;
    private List<Scheduler> mSchedulers;

    private Set<String> mCancelledIds;

    private final List<ExecutionListener> mOuterListeners;
    private final Object mLock;

    public Processor(
            Context appContext,
            Configuration configuration,
            TaskExecutor workTaskExecutor,
            WorkDatabase workDatabase,
            List<Scheduler> schedulers) {
        mAppContext = appContext;
        mConfiguration = configuration;
        mWorkTaskExecutor = workTaskExecutor;
        mWorkDatabase = workDatabase;
        mEnqueuedWorkMap = new HashMap<>();
        mSchedulers = schedulers;
        mCancelledIds = new HashSet<>();
        mOuterListeners = new ArrayList<>();
        mLock = new Object();
    }

    /**
     * Starts a given unit of work in the background.
     *
     * @param id The work id to execute.
     * @return {@code true} if the work was successfully enqueued for processing
     */
    public boolean startWork(String id) {
        return startWork(id, null);
    }

    /**
     * Starts a given unit of work in the background.
     *
     * @param id The work id to execute.
     * @param runtimeExtras The {@link WorkerParameters.RuntimeExtras} for this work, if any.
     * @return {@code true} if the work was successfully enqueued for processing
     */
    public boolean startWork(String id, WorkerParameters.RuntimeExtras runtimeExtras) {
        WorkerWrapper workWrapper;
        synchronized (mLock) {
            // Work may get triggered multiple times if they have passing constraints
            // and new work with those constraints are added.
            if (mEnqueuedWorkMap.containsKey(id)) {
                Logger.get().debug(
                        TAG,
                        String.format("Work %s is already enqueued for processing", id));
                return false;
            }

            workWrapper =
                    new WorkerWrapper.Builder(
                            mAppContext,
                            mConfiguration,
                            mWorkTaskExecutor,
                            mWorkDatabase,
                            id)
                            .withSchedulers(mSchedulers)
                            .withRuntimeExtras(runtimeExtras)
                            .build();
            ListenableFuture<Boolean> future = workWrapper.getFuture();
            future.addListener(
                    new FutureListener(this, id, future),
                    mWorkTaskExecutor.getMainThreadExecutor());
            mEnqueuedWorkMap.put(id, workWrapper);
        }
        mWorkTaskExecutor.getBackgroundExecutor().execute(workWrapper);
        Logger.get().debug(TAG, String.format("%s: processing %s", getClass().getSimpleName(), id));
        return true;
    }

    /**
     * Stops a unit of work.
     *
     * @param id The work id to stop
     * @return {@code true} if the work was stopped successfully
     */
    public boolean stopWork(String id) {
        synchronized (mLock) {
            Logger.get().debug(TAG, String.format("Processor stopping %s", id));
            WorkerWrapper wrapper = mEnqueuedWorkMap.remove(id);
            if (wrapper != null) {
                wrapper.interrupt(false);
                Logger.get().debug(TAG, String.format("WorkerWrapper stopped for %s", id));
                return true;
            }
            Logger.get().debug(TAG, String.format("WorkerWrapper could not be found for %s", id));
            return false;
        }
    }

    /**
     * Stops a unit of work and marks it as cancelled.
     *
     * @param id The work id to stop and cancel
     * @return {@code true} if the work was stopped successfully
     */
    public boolean stopAndCancelWork(String id) {
        synchronized (mLock) {
            Logger.get().debug(TAG, String.format("Processor cancelling %s", id));
            mCancelledIds.add(id);
            WorkerWrapper wrapper = mEnqueuedWorkMap.remove(id);
            if (wrapper != null) {
                wrapper.interrupt(true);
                Logger.get().debug(TAG, String.format("WorkerWrapper cancelled for %s", id));
                return true;
            }
            Logger.get().debug(TAG, String.format("WorkerWrapper could not be found for %s", id));
            return false;
        }
    }

    /**
     * Determines if the given {@code id} is marked as cancelled.
     *
     * @param id The work id to query
     * @return {@code true} if the id has already been marked as cancelled
     */
    public boolean isCancelled(String id) {
        synchronized (mLock) {
            return mCancelledIds.contains(id);
        }
    }

    /**
     * @return {@code true} if the processor has work to process.
     */
    public boolean hasWork() {
        synchronized (mLock) {
            return !mEnqueuedWorkMap.isEmpty();
        }
    }

    /**
     * @param workSpecId The {@link androidx.work.impl.model.WorkSpec} id
     * @return {@code true} if the id was enqueued in the processor.
     */
    public boolean isEnqueued(@NonNull String workSpecId) {
        synchronized (mLock) {
            return mEnqueuedWorkMap.containsKey(workSpecId);
        }
    }

    /**
     * Adds an {@link ExecutionListener} to track when work finishes.
     *
     * @param executionListener The {@link ExecutionListener} to add
     */
    public void addExecutionListener(ExecutionListener executionListener) {
        synchronized (mLock) {
            mOuterListeners.add(executionListener);
        }
    }

    /**
     * Removes a tracked {@link ExecutionListener}.
     *
     * @param executionListener The {@link ExecutionListener} to remove
     */
    public void removeExecutionListener(ExecutionListener executionListener) {
        synchronized (mLock) {
            mOuterListeners.remove(executionListener);
        }
    }

    @Override
    public void onExecuted(
            @NonNull final String workSpecId,
            boolean needsReschedule) {

        synchronized (mLock) {
            mEnqueuedWorkMap.remove(workSpecId);
            Logger.get().debug(TAG, String.format("%s %s executed; reschedule = %s",
                    getClass().getSimpleName(), workSpecId, needsReschedule));

            for (ExecutionListener executionListener : mOuterListeners) {
                executionListener.onExecuted(workSpecId, needsReschedule);
            }
        }
    }

    /**
     * An {@link ExecutionListener} for the {@link ListenableFuture} returned by
     * {@link WorkerWrapper}.
     */
    private static class FutureListener implements Runnable {

        private @NonNull ExecutionListener mExecutionListener;
        private @NonNull String mWorkSpecId;
        private @NonNull ListenableFuture<Boolean> mFuture;

        FutureListener(
                @NonNull ExecutionListener executionListener,
                @NonNull String workSpecId,
                @NonNull ListenableFuture<Boolean> future) {
            mExecutionListener = executionListener;
            mWorkSpecId = workSpecId;
            mFuture = future;
        }

        @Override
        public void run() {
            boolean needsReschedule;
            try {
                needsReschedule = mFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                // Should never really happen(?)
                needsReschedule = true;
            }
            mExecutionListener.onExecuted(mWorkSpecId, needsReschedule);
        }
    }
}