blob: 9641282a02db46c376c5ff3ff2e2e2bca94eb634 [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 android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import androidx.work.BaseWork;
import androidx.work.ExistingWorkPolicy;
import androidx.work.State;
import androidx.work.impl.InternalWorkImpl;
import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkContinuationImpl;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.logger.Logger;
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;
/**
* 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;
private final List<InternalWorkImpl> mWorkToBeScheduled;
public EnqueueRunnable(@NonNull WorkContinuationImpl workContinuation) {
mWorkContinuation = workContinuation;
mWorkToBeScheduled = new ArrayList<>();
}
@Override
public void run() {
addToDatabase();
scheduleWorkInBackground();
}
/**
* Adds the {@link WorkSpec}'s to the datastore, parent first.
* Schedules work on the background scheduler, if transaction is successful.
*/
@VisibleForTesting
public void addToDatabase() {
WorkManagerImpl workManagerImpl = mWorkContinuation.getWorkManagerImpl();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
workDatabase.beginTransaction();
try {
processContinuation(mWorkContinuation, mWorkToBeScheduled);
workDatabase.setTransactionSuccessful();
} finally {
workDatabase.endTransaction();
}
}
/**
* Schedules work on the background scheduler.
*/
@VisibleForTesting
public void scheduleWorkInBackground() {
// Schedule in the background. This list contains work that does not have prerequisites.
for (Scheduler scheduler : mWorkContinuation.getWorkManagerImpl().getSchedulers()) {
for (InternalWorkImpl work : mWorkToBeScheduled) {
scheduler.schedule(work.getWorkSpec());
}
}
}
private static void processContinuation(
@NonNull WorkContinuationImpl workContinuation,
@NonNull List<InternalWorkImpl> workToBeScheduled) {
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()) {
processContinuation(parent, workToBeScheduled);
} else {
Logger.warn(TAG, "Already enqueued work ids (%s).",
TextUtils.join(", ", parent.getIds()));
}
}
}
enqueueContinuation(workContinuation, workToBeScheduled);
}
private static void enqueueContinuation(
@NonNull WorkContinuationImpl workContinuation,
@NonNull List<InternalWorkImpl> workToBeScheduled) {
List<WorkContinuationImpl> parents = workContinuation.getParents();
Set<String> prerequisiteIds = new HashSet<>();
if (parents != null) {
for (WorkContinuationImpl parent : parents) {
prerequisiteIds.addAll(parent.getIds());
}
}
enqueueWorkWithPrerequisites(
workContinuation.getWorkManagerImpl(),
workContinuation.getWork(),
prerequisiteIds.toArray(new String[0]),
workContinuation.getName(),
workContinuation.getExistingWorkPolicy(),
workToBeScheduled);
workContinuation.markEnqueued();
}
/**
* Enqueues the {@link WorkSpec}'s while keeping track of the prerequisites.
*/
private static void enqueueWorkWithPrerequisites(
WorkManagerImpl workManagerImpl,
@NonNull List<? extends BaseWork> workList,
String[] prerequisiteIds,
String name,
ExistingWorkPolicy existingWorkPolicy,
@NonNull List<InternalWorkImpl> workToBeScheduled) {
long currentTimeMillis = System.currentTimeMillis();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
List<InternalWorkImpl> workImplList = new ArrayList<>(workList.size());
for (int i = 0; i < workList.size(); ++i) {
workImplList.add((InternalWorkImpl) workList.get(i));
}
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) {
Logger.error(TAG, "Prerequisite %s doesn't exist; not enqueuing", id);
return;
}
State prerequisiteState = prerequisiteWorkSpec.getState();
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;
}
}
}
// 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);
}
}
}
}
for (InternalWorkImpl work : workImplList) {
WorkSpec workSpec = work.getWorkSpec();
if (hasPrerequisite && !hasCompletedAllPrerequisites) {
if (hasFailedPrerequisites) {
workSpec.setState(FAILED);
} else if (hasCancelledPrerequisites) {
workSpec.setState(CANCELLED);
} else {
workSpec.setState(BLOCKED);
}
} else {
// Set scheduled times only for work without prerequisites. Dependent work
// will set their scheduled times when they are unblocked.
workSpec.setPeriodStartTime(currentTimeMillis);
}
workDatabase.workSpecDao().insertWorkSpec(workSpec);
if (hasPrerequisite) {
for (String prerequisiteId : prerequisiteIds) {
Dependency dep = new Dependency(work.getId(), prerequisiteId);
workDatabase.dependencyDao().insertDependency(dep);
}
}
for (String tag : work.getTags()) {
workDatabase.workTagDao().insert(new WorkTag(tag, work.getId()));
}
if (isNamed) {
workDatabase.workNameDao().insert(new WorkName(name, work.getId()));
}
}
// If there are no prerequisites (or they have all been completed), then add then to the
// list of work items that can be scheduled in the background. The actual scheduling is
// typically done after the transaction has settled.
if (!hasPrerequisite || hasCompletedAllPrerequisites) {
workToBeScheduled.addAll(workImplList);
}
}
}