| /* |
| * 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.background.greedy; |
| |
| import static android.os.Build.VERSION.SDK_INT; |
| |
| import android.content.Context; |
| import android.text.TextUtils; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.RestrictTo; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.work.Configuration; |
| import androidx.work.Logger; |
| import androidx.work.WorkInfo; |
| import androidx.work.impl.ExecutionListener; |
| import androidx.work.impl.Scheduler; |
| import androidx.work.impl.WorkManagerImpl; |
| import androidx.work.impl.constraints.WorkConstraintsCallback; |
| import androidx.work.impl.constraints.WorkConstraintsTracker; |
| import androidx.work.impl.model.WorkSpec; |
| import androidx.work.impl.utils.ProcessUtils; |
| import androidx.work.impl.utils.taskexecutor.TaskExecutor; |
| |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * A greedy {@link Scheduler} that schedules unconstrained, non-timed work. It intentionally does |
| * not acquire any WakeLocks, instead trying to brute-force them as time allows before the process |
| * gets killed. |
| * |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| public class GreedyScheduler implements Scheduler, WorkConstraintsCallback, ExecutionListener { |
| |
| private static final String TAG = Logger.tagWithPrefix("GreedyScheduler"); |
| |
| private final Context mContext; |
| private final WorkManagerImpl mWorkManagerImpl; |
| private final WorkConstraintsTracker mWorkConstraintsTracker; |
| private final Set<WorkSpec> mConstrainedWorkSpecs = new HashSet<>(); |
| private DelayedWorkTracker mDelayedWorkTracker; |
| private boolean mRegisteredExecutionListener; |
| private final Object mLock; |
| |
| // Internal State |
| Boolean mInDefaultProcess; |
| |
| public GreedyScheduler( |
| @NonNull Context context, |
| @NonNull Configuration configuration, |
| @NonNull TaskExecutor taskExecutor, |
| @NonNull WorkManagerImpl workManagerImpl) { |
| mContext = context; |
| mWorkManagerImpl = workManagerImpl; |
| mWorkConstraintsTracker = new WorkConstraintsTracker(context, taskExecutor, this); |
| mDelayedWorkTracker = new DelayedWorkTracker(this, configuration.getRunnableScheduler()); |
| mLock = new Object(); |
| } |
| |
| @VisibleForTesting |
| public GreedyScheduler( |
| @NonNull Context context, |
| @NonNull WorkManagerImpl workManagerImpl, |
| @NonNull WorkConstraintsTracker workConstraintsTracker) { |
| mContext = context; |
| mWorkManagerImpl = workManagerImpl; |
| mWorkConstraintsTracker = workConstraintsTracker; |
| mLock = new Object(); |
| } |
| |
| @VisibleForTesting |
| public void setDelayedWorkTracker(@NonNull DelayedWorkTracker delayedWorkTracker) { |
| mDelayedWorkTracker = delayedWorkTracker; |
| } |
| |
| @Override |
| public boolean hasLimitedSchedulingSlots() { |
| return false; |
| } |
| |
| @Override |
| public void schedule(@NonNull WorkSpec... workSpecs) { |
| if (mInDefaultProcess == null) { |
| checkDefaultProcess(); |
| } |
| |
| if (!mInDefaultProcess) { |
| Logger.get().info(TAG, "Ignoring schedule request in a secondary process"); |
| return; |
| } |
| |
| registerExecutionListenerIfNeeded(); |
| |
| // Keep track of the list of new WorkSpecs whose constraints need to be tracked. |
| // Add them to the known list of constrained WorkSpecs and call replace() on |
| // WorkConstraintsTracker. That way we only need to synchronize on the part where we |
| // are updating mConstrainedWorkSpecs. |
| Set<WorkSpec> constrainedWorkSpecs = new HashSet<>(); |
| Set<String> constrainedWorkSpecIds = new HashSet<>(); |
| |
| for (WorkSpec workSpec : workSpecs) { |
| long nextRunTime = workSpec.calculateNextRunTime(); |
| long now = System.currentTimeMillis(); |
| if (workSpec.state == WorkInfo.State.ENQUEUED) { |
| if (now < nextRunTime) { |
| // Future work |
| if (mDelayedWorkTracker != null) { |
| mDelayedWorkTracker.schedule(workSpec); |
| } |
| } else if (workSpec.hasConstraints()) { |
| if (SDK_INT >= 23 && workSpec.constraints.requiresDeviceIdle()) { |
| // Ignore requests that have an idle mode constraint. |
| Logger.get().debug(TAG, |
| String.format("Ignoring WorkSpec %s, Requires device idle.", |
| workSpec)); |
| } else if (SDK_INT >= 24 && workSpec.constraints.hasContentUriTriggers()) { |
| // Ignore requests that have content uri triggers. |
| Logger.get().debug(TAG, |
| String.format("Ignoring WorkSpec %s, Requires ContentUri triggers.", |
| workSpec)); |
| } else { |
| constrainedWorkSpecs.add(workSpec); |
| constrainedWorkSpecIds.add(workSpec.id); |
| } |
| } else { |
| Logger.get().debug(TAG, String.format("Starting work for %s", workSpec.id)); |
| mWorkManagerImpl.startWork(workSpec.id); |
| } |
| } |
| } |
| |
| // onExecuted() which is called on the main thread also modifies the list of mConstrained |
| // WorkSpecs. Therefore we need to lock here. |
| synchronized (mLock) { |
| if (!constrainedWorkSpecs.isEmpty()) { |
| Logger.get().debug(TAG, String.format("Starting tracking for [%s]", |
| TextUtils.join(",", constrainedWorkSpecIds))); |
| mConstrainedWorkSpecs.addAll(constrainedWorkSpecs); |
| mWorkConstraintsTracker.replace(mConstrainedWorkSpecs); |
| } |
| } |
| } |
| |
| private void checkDefaultProcess() { |
| Configuration configuration = mWorkManagerImpl.getConfiguration(); |
| mInDefaultProcess = ProcessUtils.isDefaultProcess(mContext, configuration); |
| } |
| |
| @Override |
| public void cancel(@NonNull String workSpecId) { |
| if (mInDefaultProcess == null) { |
| checkDefaultProcess(); |
| } |
| |
| if (!mInDefaultProcess) { |
| Logger.get().info(TAG, "Ignoring schedule request in non-main process"); |
| return; |
| } |
| |
| registerExecutionListenerIfNeeded(); |
| Logger.get().debug(TAG, String.format("Cancelling work ID %s", workSpecId)); |
| if (mDelayedWorkTracker != null) { |
| mDelayedWorkTracker.unschedule(workSpecId); |
| } |
| // onExecutionCompleted does the cleanup. |
| mWorkManagerImpl.stopWork(workSpecId); |
| } |
| |
| @Override |
| public void onAllConstraintsMet(@NonNull List<String> workSpecIds) { |
| for (String workSpecId : workSpecIds) { |
| Logger.get().debug( |
| TAG, |
| String.format("Constraints met: Scheduling work ID %s", workSpecId)); |
| mWorkManagerImpl.startWork(workSpecId); |
| } |
| } |
| |
| @Override |
| public void onAllConstraintsNotMet(@NonNull List<String> workSpecIds) { |
| for (String workSpecId : workSpecIds) { |
| Logger.get().debug(TAG, |
| String.format("Constraints not met: Cancelling work ID %s", workSpecId)); |
| mWorkManagerImpl.stopWork(workSpecId); |
| } |
| } |
| |
| @Override |
| public void onExecuted(@NonNull String workSpecId, boolean needsReschedule) { |
| removeConstraintTrackingFor(workSpecId); |
| // onExecuted does not need to worry about unscheduling WorkSpecs with the mDelayedTracker. |
| // This is because, after onExecuted(), all schedulers are asked to cancel. |
| } |
| |
| private void removeConstraintTrackingFor(@NonNull String workSpecId) { |
| synchronized (mLock) { |
| // This is synchronized because onExecuted is on the main thread but |
| // Schedulers#schedule() can modify the list of mConstrainedWorkSpecs on the task |
| // executor thread. |
| for (WorkSpec constrainedWorkSpec : mConstrainedWorkSpecs) { |
| if (constrainedWorkSpec.id.equals(workSpecId)) { |
| Logger.get().debug(TAG, String.format("Stopping tracking for %s", workSpecId)); |
| mConstrainedWorkSpecs.remove(constrainedWorkSpec); |
| mWorkConstraintsTracker.replace(mConstrainedWorkSpecs); |
| break; |
| } |
| } |
| } |
| } |
| |
| private void registerExecutionListenerIfNeeded() { |
| // This method needs to be called *after* Processor is created, since Processor needs |
| // Schedulers and is created after this class. |
| if (!mRegisteredExecutionListener) { |
| mWorkManagerImpl.getProcessor().addExecutionListener(this); |
| mRegisteredExecutionListener = true; |
| } |
| } |
| } |