[go: nahoru, domu]

blob: d0f147936d84a3a5dd80874d8760f743266fdbdc [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.impl.foreground.SystemForegroundDispatcher.createStartForegroundIntent;
import static androidx.work.impl.foreground.SystemForegroundDispatcher.createStopForegroundIntent;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
import androidx.work.Configuration;
import androidx.work.ForegroundInfo;
import androidx.work.Logger;
import androidx.work.WorkerParameters;
import androidx.work.impl.foreground.ForegroundProcessor;
import androidx.work.impl.model.WorkGenerationalId;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.WakeLocks;
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, ForegroundProcessor {
private static final String TAG = Logger.tagWithPrefix("Processor");
private static final String FOREGROUND_WAKELOCK_TAG = "ProcessorForegroundLck";
@Nullable
private PowerManager.WakeLock mForegroundLock;
private Context mAppContext;
private Configuration mConfiguration;
private TaskExecutor mWorkTaskExecutor;
private WorkDatabase mWorkDatabase;
private Map<String, WorkerWrapper> mForegroundWorkMap;
private Map<String, WorkerWrapper> mEnqueuedWorkMap;
// workSpecId to a Set<WorkRunId>
private Map<String, Set<StartStopToken>> mWorkRuns;
private List<Scheduler> mSchedulers;
private Set<String> mCancelledIds;
private final List<ExecutionListener> mOuterListeners;
private final Object mLock;
public Processor(
@NonNull Context appContext,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
@NonNull WorkDatabase workDatabase,
@NonNull List<Scheduler> schedulers) {
mAppContext = appContext;
mConfiguration = configuration;
mWorkTaskExecutor = workTaskExecutor;
mWorkDatabase = workDatabase;
mEnqueuedWorkMap = new HashMap<>();
mForegroundWorkMap = new HashMap<>();
mSchedulers = schedulers;
mCancelledIds = new HashSet<>();
mOuterListeners = new ArrayList<>();
mForegroundLock = null;
mLock = new Object();
mWorkRuns = new HashMap<>();
}
/**
* 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(@NonNull StartStopToken id) {
return startWork(id, null);
}
/**
* Starts a given unit of work in the background.
*
* @param startStopToken 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
*/
@SuppressWarnings("ConstantConditions")
public boolean startWork(
@NonNull StartStopToken startStopToken,
@Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
WorkGenerationalId id = startStopToken.getId();
WorkSpec workSpec = mWorkDatabase.runInTransaction(
() -> mWorkDatabase.workSpecDao().getWorkSpec(id.getWorkSpecId())
);
if (workSpec == null) {
Logger.get().warning(TAG, "Didn't find WorkSpec for id " + id);
runOnExecuted(id, false);
return false;
}
WorkerWrapper workWrapper;
synchronized (mLock) {
// Work may get triggered multiple times if they have passing constraints
// and new work with those constraints are added.
String workSpecId = id.getWorkSpecId();
if (isEnqueued(workSpecId)) {
// there must be another run if it is enqueued.
Set<StartStopToken> tokens = mWorkRuns.get(workSpecId);
StartStopToken previousRun = tokens.iterator().next();
int previousRunGeneration = previousRun.getId().getGeneration();
if (previousRunGeneration == id.getGeneration()) {
tokens.add(startStopToken);
Logger.get().debug(TAG, "Work " + id + " is already enqueued for processing");
} else {
// Implementation detail.
// If previousRunGeneration > id.getGeneration(), then we don't have to do
// anything because newer generation is already running
//
// Case of previousRunGeneration < id.getGeneration():
// it should happen only in the case of the periodic worker,
// so we let run a current Worker, and periodic worker will schedule
// next iteration with updated work spec.
runOnExecuted(id, false);
}
return false;
}
if (workSpec.getGeneration() != id.getGeneration()) {
// not the latest generation, so ignoring this start request,
// new request with newer generation should arrive shortly.
runOnExecuted(id, false);
return false;
}
workWrapper =
new WorkerWrapper.Builder(
mAppContext,
mConfiguration,
mWorkTaskExecutor,
this,
mWorkDatabase,
workSpec)
.withSchedulers(mSchedulers)
.withRuntimeExtras(runtimeExtras)
.build();
ListenableFuture<Boolean> future = workWrapper.getFuture();
future.addListener(
new FutureListener(this, startStopToken.getId(), future),
mWorkTaskExecutor.getMainThreadExecutor());
mEnqueuedWorkMap.put(workSpecId, workWrapper);
HashSet<StartStopToken> set = new HashSet<>();
set.add(startStopToken);
mWorkRuns.put(workSpecId, set);
}
mWorkTaskExecutor.getSerialTaskExecutor().execute(workWrapper);
Logger.get().debug(TAG, getClass().getSimpleName() + ": processing " + id);
return true;
}
@Override
public void startForeground(@NonNull String workSpecId,
@NonNull ForegroundInfo foregroundInfo) {
synchronized (mLock) {
Logger.get().info(TAG, "Moving WorkSpec (" + workSpecId + ") to the foreground");
WorkerWrapper wrapper = mEnqueuedWorkMap.remove(workSpecId);
if (wrapper != null) {
if (mForegroundLock == null) {
mForegroundLock = WakeLocks.newWakeLock(mAppContext, FOREGROUND_WAKELOCK_TAG);
mForegroundLock.acquire();
}
mForegroundWorkMap.put(workSpecId, wrapper);
Intent intent = createStartForegroundIntent(mAppContext,
wrapper.getWorkGenerationalId(), foregroundInfo);
ContextCompat.startForegroundService(mAppContext, intent);
}
}
}
/**
* Stops a unit of work running in the context of a foreground service.
*
* @param token The work to stop
* @return {@code true} if the work was stopped successfully
*/
public boolean stopForegroundWork(@NonNull StartStopToken token) {
String id = token.getId().getWorkSpecId();
WorkerWrapper wrapper = null;
synchronized (mLock) {
Logger.get().debug(TAG, "Processor stopping foreground work " + id);
wrapper = mForegroundWorkMap.remove(id);
if (wrapper != null) {
mWorkRuns.remove(id);
}
}
// Move interrupt() outside the critical section.
// This is because calling interrupt() eventually calls ListenableWorker.onStopped()
// If onStopped() takes too long, there is a good chance this causes an ANR
// in Processor.onExecuted().
return interrupt(id, wrapper);
}
/**
* Stops a unit of work.
*
* @param runId The work id to stop
* @return {@code true} if the work was stopped successfully
*/
public boolean stopWork(@NonNull StartStopToken runId) {
String id = runId.getId().getWorkSpecId();
WorkerWrapper wrapper = null;
synchronized (mLock) {
// Processor _only_ receives stopWork() requests from the schedulers that originally
// scheduled the work, and not others. This means others are still notified about
// completion, but we avoid a accidental "stops" and lot of redundant work when
// attempting to stop.
wrapper = mEnqueuedWorkMap.remove(id);
if (wrapper == null) {
Logger.get().debug(TAG, "WorkerWrapper could not be found for " + id);
return false;
}
Set<StartStopToken> runs = mWorkRuns.get(id);
if (runs == null || !runs.contains(runId)) {
return false;
}
Logger.get().debug(TAG, "Processor stopping background work " + id);
mWorkRuns.remove(id);
}
// Move interrupt() outside the critical section.
// This is because calling interrupt() eventually calls ListenableWorker.onStopped()
// If onStopped() takes too long, there is a good chance this causes an ANR
// in Processor.onExecuted().
return interrupt(id, wrapper);
}
/**
* 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(@NonNull String id) {
WorkerWrapper wrapper = null;
boolean isForegroundWork = false;
synchronized (mLock) {
Logger.get().debug(TAG, "Processor cancelling " + id);
mCancelledIds.add(id);
// Check if running in the context of a foreground service
wrapper = mForegroundWorkMap.remove(id);
isForegroundWork = wrapper != null;
if (wrapper == null) {
// Fallback to enqueued Work
wrapper = mEnqueuedWorkMap.remove(id);
}
if (wrapper != null) {
mWorkRuns.remove(id);
}
}
// Move interrupt() outside the critical section.
// This is because calling interrupt() eventually calls ListenableWorker.onStopped()
// If onStopped() takes too long, there is a good chance this causes an ANR
// in Processor.onExecuted().
boolean interrupted = interrupt(id, wrapper);
if (isForegroundWork) {
stopForegroundService();
}
return interrupted;
}
@Override
public void stopForeground(@NonNull String workSpecId) {
synchronized (mLock) {
mForegroundWorkMap.remove(workSpecId);
stopForegroundService();
}
}
/**
* 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(@NonNull 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()
&& mForegroundWorkMap.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)
|| mForegroundWorkMap.containsKey(workSpecId);
}
}
/**
* @param workSpecId The {@link androidx.work.impl.model.WorkSpec} id
* @return {@code true} if the id was enqueued as foreground work in the processor.
*/
@Override
public boolean isEnqueuedInForeground(@NonNull String workSpecId) {
synchronized (mLock) {
return mForegroundWorkMap.containsKey(workSpecId);
}
}
/**
* Adds an {@link ExecutionListener} to track when work finishes.
*
* @param executionListener The {@link ExecutionListener} to add
*/
public void addExecutionListener(@NonNull ExecutionListener executionListener) {
synchronized (mLock) {
mOuterListeners.add(executionListener);
}
}
/**
* Removes a tracked {@link ExecutionListener}.
*
* @param executionListener The {@link ExecutionListener} to remove
*/
public void removeExecutionListener(@NonNull ExecutionListener executionListener) {
synchronized (mLock) {
mOuterListeners.remove(executionListener);
}
}
@Override
public void onExecuted(@NonNull final WorkGenerationalId id, boolean needsReschedule) {
synchronized (mLock) {
WorkerWrapper workerWrapper = mEnqueuedWorkMap.get(id.getWorkSpecId());
// can be called for another generation, so we shouldn't removed
if (workerWrapper != null && id.equals(workerWrapper.getWorkGenerationalId())) {
mEnqueuedWorkMap.remove(id.getWorkSpecId());
}
Logger.get().debug(TAG,
getClass().getSimpleName() + " " + id.getWorkSpecId()
+ " executed; reschedule = " + needsReschedule);
for (ExecutionListener executionListener : mOuterListeners) {
executionListener.onExecuted(id, needsReschedule);
}
}
}
private void runOnExecuted(@NonNull final WorkGenerationalId id, boolean needsReschedule) {
mWorkTaskExecutor.getMainThreadExecutor().execute(
() -> onExecuted(id, needsReschedule)
);
}
private void stopForegroundService() {
synchronized (mLock) {
boolean hasForegroundWork = !mForegroundWorkMap.isEmpty();
if (!hasForegroundWork) {
Intent intent = createStopForegroundIntent(mAppContext);
try {
// Wrapping this inside a try..catch, because there are bugs the platform
// that cause an IllegalStateException when an intent is dispatched to stop
// the foreground service that is running.
mAppContext.startService(intent);
} catch (Throwable throwable) {
Logger.get().error(TAG, "Unable to stop foreground service", throwable);
}
// Release wake lock if there is no more pending work.
if (mForegroundLock != null) {
mForegroundLock.release();
mForegroundLock = null;
}
}
}
}
/**
* Interrupts a unit of work.
*
* @param id The {@link androidx.work.impl.model.WorkSpec} id
* @param wrapper The {@link WorkerWrapper}
* @return {@code true} if the work was stopped successfully
*/
private static boolean interrupt(@NonNull String id, @Nullable WorkerWrapper wrapper) {
if (wrapper != null) {
wrapper.interrupt();
Logger.get().debug(TAG, "WorkerWrapper interrupted for " + id);
return true;
} else {
Logger.get().debug(TAG, "WorkerWrapper could not be found for " + id);
return false;
}
}
/**
* An {@link ExecutionListener} for the {@link ListenableFuture} returned by
* {@link WorkerWrapper}.
*/
private static class FutureListener implements Runnable {
private @NonNull ExecutionListener mExecutionListener;
private @NonNull final WorkGenerationalId mWorkGenerationalId;
private @NonNull ListenableFuture<Boolean> mFuture;
FutureListener(
@NonNull ExecutionListener executionListener,
@NonNull WorkGenerationalId workGenerationalId,
@NonNull ListenableFuture<Boolean> future) {
mExecutionListener = executionListener;
mWorkGenerationalId = workGenerationalId;
mFuture = future;
}
@Override
public void run() {
boolean needsReschedule;
try {
needsReschedule = mFuture.get();
} catch (InterruptedException | ExecutionException e) {
// Should never really happen(?)
needsReschedule = true;
}
mExecutionListener.onExecuted(mWorkGenerationalId, needsReschedule);
}
}
}