blob: 004b895b89401798cf4a9ef56c098a0cca42fb7b [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.utils;
import static androidx.work.ExistingWorkPolicy.APPEND;
import static androidx.work.ExistingWorkPolicy.KEEP;
import static androidx.work.State.BLOCKED;
import static androidx.work.State.CANCELLED;
import static androidx.work.State.ENQUEUED;
import static androidx.work.State.FAILED;
import static androidx.work.State.RUNNING;
import static androidx.work.State.SUCCEEDED;
import static androidx.work.impl.workers.ConstraintTrackingWorker.ARGUMENT_CLASS_NAME;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.State;
import androidx.work.WorkRequest;
import androidx.work.impl.Schedulers;
import androidx.work.impl.WorkContinuationImpl;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.Dependency;
import androidx.work.impl.model.DependencyDao;
import androidx.work.impl.model.WorkName;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.model.WorkSpecDao;
import androidx.work.impl.model.WorkTag;
import androidx.work.impl.workers.ConstraintTrackingWorker;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Manages the enqueuing of a {@link WorkContinuationImpl}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class EnqueueRunnable implements Runnable {
private static final String TAG = "EnqueueRunnable";
private final WorkContinuationImpl mWorkContinuation;
public EnqueueRunnable(@NonNull WorkContinuationImpl workContinuation) {
mWorkContinuation = workContinuation;
}
@Override
public void run() {
if (mWorkContinuation.hasCycles()) {
throw new IllegalStateException(
String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
}
boolean needsScheduling = addToDatabase();
if (needsScheduling) {
scheduleWorkInBackground();
}
}
/**
* Adds the {@link WorkSpec}'s to the datastore, parent first.
* Schedules work on the background scheduler, if transaction is successful.
*/
@VisibleForTesting
public boolean addToDatabase() {
WorkManagerImpl workManagerImpl = mWorkContinuation.getWorkManagerImpl();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
workDatabase.beginTransaction();
try {
boolean needsScheduling = processContinuation(mWorkContinuation);
workDatabase.setTransactionSuccessful();
return needsScheduling;
} finally {
workDatabase.endTransaction();
}
}
/**
* Schedules work on the background scheduler.
*/
@VisibleForTesting
public void scheduleWorkInBackground() {
WorkManagerImpl workManager = mWorkContinuation.getWorkManagerImpl();
Schedulers.schedule(workManager.getWorkDatabase(), workManager.getSchedulers());
}
private static boolean processContinuation(@NonNull WorkContinuationImpl workContinuation) {
boolean needsScheduling = false;
List<WorkContinuationImpl> parents = workContinuation.getParents();
if (parents != null) {
for (WorkContinuationImpl parent : parents) {
// When chaining off a completed continuation we need to pay
// attention to parents that may have been marked as enqueued before.
if (!parent.isEnqueued()) {
needsScheduling |= processContinuation(parent);
} else {
Log.w(TAG, String.format("Already enqueued work ids (%s).",
TextUtils.join(", ", parent.getIds())));
}
}
}
needsScheduling |= enqueueContinuation(workContinuation);
return needsScheduling;
}
private static boolean enqueueContinuation(@NonNull WorkContinuationImpl workContinuation) {
Set<String> prerequisiteIds = WorkContinuationImpl.prerequisitesFor(workContinuation);
boolean needsScheduling = enqueueWorkWithPrerequisites(
workContinuation.getWorkManagerImpl(),
workContinuation.getWork(),
prerequisiteIds.toArray(new String[0]),
workContinuation.getName(),
workContinuation.getExistingWorkPolicy());
workContinuation.markEnqueued();
return needsScheduling;
}
/**
* Enqueues the {@link WorkSpec}'s while keeping track of the prerequisites.
*
* @return {@code true} If there is any scheduling to be done.
*/
private static boolean enqueueWorkWithPrerequisites(
WorkManagerImpl workManagerImpl,
@NonNull List<? extends WorkRequest> workList,
String[] prerequisiteIds,
String name,
ExistingWorkPolicy existingWorkPolicy) {
long currentTimeMillis = System.currentTimeMillis();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
boolean hasPrerequisite = (prerequisiteIds != null && prerequisiteIds.length > 0);
boolean hasCompletedAllPrerequisites = true;
boolean hasFailedPrerequisites = false;
boolean hasCancelledPrerequisites = false;
if (hasPrerequisite) {
// If there are prerequisites, make sure they actually exist before enqueuing
// anything. Prerequisites may not exist if we are using unique tags, because the
// chain of work could have been wiped out already.
for (String id : prerequisiteIds) {
WorkSpec prerequisiteWorkSpec = workDatabase.workSpecDao().getWorkSpec(id);
if (prerequisiteWorkSpec == null) {
Log.e(TAG, String.format("Prerequisite %s doesn't exist; not enqueuing", id));
return false;
}
State prerequisiteState = prerequisiteWorkSpec.state;
hasCompletedAllPrerequisites &= (prerequisiteState == SUCCEEDED);
if (prerequisiteState == FAILED) {
hasFailedPrerequisites = true;
} else if (prerequisiteState == CANCELLED) {
hasCancelledPrerequisites = true;
}
}
}
boolean isNamed = !TextUtils.isEmpty(name);
// We only apply existing work policies for unique tag sequences that are the beginning of
// chains.
boolean shouldApplyExistingWorkPolicy = isNamed && !hasPrerequisite;
if (shouldApplyExistingWorkPolicy) {
// Get everything with the unique tag.
List<WorkSpec.IdAndState> existingWorkSpecIdAndStates =
workDatabase.workSpecDao().getWorkSpecIdAndStatesForName(name);
if (!existingWorkSpecIdAndStates.isEmpty()) {
// If appending, these are the new prerequisites.
if (existingWorkPolicy == APPEND) {
DependencyDao dependencyDao = workDatabase.dependencyDao();
List<String> newPrerequisiteIds = new ArrayList<>();
for (WorkSpec.IdAndState idAndState : existingWorkSpecIdAndStates) {
if (!dependencyDao.hasDependents(idAndState.id)) {
hasCompletedAllPrerequisites &= (idAndState.state == SUCCEEDED);
if (idAndState.state == FAILED) {
hasFailedPrerequisites = true;
} else if (idAndState.state == CANCELLED) {
hasCancelledPrerequisites = true;
}
newPrerequisiteIds.add(idAndState.id);
}
}
prerequisiteIds = newPrerequisiteIds.toArray(prerequisiteIds);
hasPrerequisite = (prerequisiteIds.length > 0);
} else {
// If we're keeping existing work, make sure to do so only if something is
// enqueued or running.
if (existingWorkPolicy == KEEP) {
for (WorkSpec.IdAndState idAndState : existingWorkSpecIdAndStates) {
if (idAndState.state == ENQUEUED || idAndState.state == RUNNING) {
return false;
}
}
}
// Cancel all of these workers.
CancelWorkRunnable.forName(name, workManagerImpl).run();
// And delete all the database records.
WorkSpecDao workSpecDao = workDatabase.workSpecDao();
for (WorkSpec.IdAndState idAndState : existingWorkSpecIdAndStates) {
workSpecDao.delete(idAndState.id);
}
}
}
}
boolean needsScheduling = false;
for (WorkRequest work : workList) {
WorkSpec workSpec = work.getWorkSpec();
if (hasPrerequisite && !hasCompletedAllPrerequisites) {
if (hasFailedPrerequisites) {
workSpec.state = FAILED;
} else if (hasCancelledPrerequisites) {
workSpec.state = CANCELLED;
} else {
workSpec.state = BLOCKED;
}
} else {
// Set scheduled times only for work without prerequisites. Dependent work
// will set their scheduled times when they are unblocked.
workSpec.periodStartTime = currentTimeMillis;
}
if (Build.VERSION.SDK_INT >= 23 && Build.VERSION.SDK_INT <= 25) {
tryDelegateConstrainedWorkSpec(workSpec);
}
// If we have one WorkSpec with an enqueued state, then we need to schedule.
if (workSpec.state == ENQUEUED) {
needsScheduling = true;
}
workDatabase.workSpecDao().insertWorkSpec(workSpec);
if (hasPrerequisite) {
for (String prerequisiteId : prerequisiteIds) {
Dependency dep = new Dependency(work.getStringId(), prerequisiteId);
workDatabase.dependencyDao().insertDependency(dep);
}
}
for (String tag : work.getTags()) {
workDatabase.workTagDao().insert(new WorkTag(tag, work.getStringId()));
}
if (isNamed) {
workDatabase.workNameDao().insert(new WorkName(name, work.getStringId()));
}
}
return needsScheduling;
}
private static void tryDelegateConstrainedWorkSpec(WorkSpec workSpec) {
// requiresBatteryNotLow and requiresStorageNotLow require API 26 for JobScheduler.
// Delegate to ConstraintTrackingWorker between API 23-25.
Constraints constraints = workSpec.constraints;
if (constraints.requiresBatteryNotLow() || constraints.requiresStorageNotLow()) {
String workerClassName = workSpec.workerClassName;
Data.Builder builder = new Data.Builder();
// Copy all arguments
builder.putAll(workSpec.input)
.putString(ARGUMENT_CLASS_NAME, workerClassName);
workSpec.workerClassName = ConstraintTrackingWorker.class.getName();
workSpec.input = builder.build();
}
}
}