blob: 5558b22edf0d006e1ecb3308c3925561c9b81575 [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 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);
}
}
}