1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.work.impl.background.systemalarm;
18 
19 import static androidx.work.impl.background.systemalarm.CommandHandler.WORK_PROCESSING_TIME_IN_MS;
20 import static androidx.work.impl.constraints.WorkConstraintsTrackerKt.listen;
21 
22 import android.content.Context;
23 import android.content.Intent;
24 import android.os.PowerManager;
25 
26 import androidx.annotation.RestrictTo;
27 import androidx.annotation.WorkerThread;
28 import androidx.work.Logger;
29 import androidx.work.impl.StartStopToken;
30 import androidx.work.impl.constraints.ConstraintsState;
31 import androidx.work.impl.constraints.OnConstraintsStateChangedListener;
32 import androidx.work.impl.constraints.WorkConstraintsTracker;
33 import androidx.work.impl.constraints.trackers.Trackers;
34 import androidx.work.impl.model.WorkGenerationalId;
35 import androidx.work.impl.model.WorkSpec;
36 import androidx.work.impl.utils.WakeLocks;
37 import androidx.work.impl.utils.WorkTimer;
38 
39 import kotlinx.coroutines.CoroutineDispatcher;
40 import kotlinx.coroutines.Job;
41 
42 import org.jspecify.annotations.NonNull;
43 import org.jspecify.annotations.Nullable;
44 
45 import java.util.concurrent.Executor;
46 
47 /**
48  * This is a command handler which attempts to run a work spec given its id.
49  * Also handles constraints gracefully.
50  *
51  */
52 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
53 public class DelayMetCommandHandler implements
54         OnConstraintsStateChangedListener,
55         WorkTimer.TimeLimitExceededListener {
56 
57     private static final String TAG = Logger.tagWithPrefix("DelayMetCommandHandler");
58 
59     /**
60      * The initial state of the delay met command handler.
61      * The handler always starts off at this state.
62      */
63     private static final int STATE_INITIAL = 0;
64     /**
65      * The command handler moves to STATE_START_REQUESTED when all constraints are met.
66      * This should only happen once per instance of the command handler.
67      */
68     private static final int STATE_START_REQUESTED = 1;
69     /**
70      * The command handler moves to STATE_STOP_REQUESTED when some constraints are unmet.
71      * This should only happen once per instance of the command handler.
72      */
73     private static final int STATE_STOP_REQUESTED = 2;
74 
75     /**
76      * State Transitions.
77      *
78      *
79      *                   |----> STATE_STOP_REQUESTED
80      *                   |
81      *                   |
82      * STATE_INITIAL---->|
83      *                   |
84      *                   |
85      *                   |----> STATE_START_REQUESTED ---->STATE_STOP_REQUESTED
86      *
87      */
88 
89     private final Context mContext;
90     private final int mStartId;
91     private final WorkGenerationalId mWorkGenerationalId;
92     private final SystemAlarmDispatcher mDispatcher;
93     private final WorkConstraintsTracker mWorkConstraintsTracker;
94     private final Object mLock;
95     // should be accessed only from SerialTaskExecutor
96     private int mCurrentState;
97     private final Executor mSerialExecutor;
98     private final Executor mMainThreadExecutor;
99 
100     private PowerManager.@Nullable WakeLock mWakeLock;
101     private boolean mHasConstraints;
102     private final StartStopToken mToken;
103     private final CoroutineDispatcher mCoroutineDispatcher;
104 
105     private volatile Job mJob;
106 
DelayMetCommandHandler( @onNull Context context, int startId, @NonNull SystemAlarmDispatcher dispatcher, @NonNull StartStopToken startStopToken)107     DelayMetCommandHandler(
108             @NonNull Context context,
109             int startId,
110             @NonNull SystemAlarmDispatcher dispatcher,
111             @NonNull StartStopToken startStopToken) {
112         mContext = context;
113         mStartId = startId;
114         mDispatcher = dispatcher;
115         mWorkGenerationalId = startStopToken.getId();
116         mToken = startStopToken;
117         Trackers trackers = dispatcher.getWorkManager().getTrackers();
118         mSerialExecutor = dispatcher.getTaskExecutor().getSerialTaskExecutor();
119         mMainThreadExecutor = dispatcher.getTaskExecutor().getMainThreadExecutor();
120         mCoroutineDispatcher = dispatcher.getTaskExecutor().getTaskCoroutineDispatcher();
121         mWorkConstraintsTracker = new WorkConstraintsTracker(trackers);
122         mHasConstraints = false;
123         mCurrentState = STATE_INITIAL;
124         mLock = new Object();
125     }
126 
127     @Override
onConstraintsStateChanged(@onNull WorkSpec workSpec, @NonNull ConstraintsState state)128     public void onConstraintsStateChanged(@NonNull WorkSpec workSpec,
129             @NonNull ConstraintsState state) {
130         if (state instanceof ConstraintsState.ConstraintsMet) {
131             mSerialExecutor.execute(this::startWork);
132         } else {
133             mSerialExecutor.execute(this::stopWork);
134         }
135     }
136 
startWork()137     private void startWork() {
138         if (mCurrentState == STATE_INITIAL) {
139             mCurrentState = STATE_START_REQUESTED;
140 
141             Logger.get().debug(TAG, "onAllConstraintsMet for " + mWorkGenerationalId);
142             // Constraints met, schedule execution
143             // Not using WorkManagerImpl#startWork() here because we need to know if the
144             // processor actually enqueued the work here.
145             boolean isEnqueued = mDispatcher.getProcessor().startWork(mToken);
146 
147             if (isEnqueued) {
148                 // setup timers to enforce quotas on workers that have
149                 // been enqueued
150                 mDispatcher.getWorkTimer()
151                         .startTimer(mWorkGenerationalId, WORK_PROCESSING_TIME_IN_MS, this);
152             } else {
153                 // if we did not actually enqueue the work, it was enqueued before
154                 // cleanUp and pretend this never happened.
155                 cleanUp();
156             }
157         } else {
158             Logger.get().debug(TAG, "Already started work for " + mWorkGenerationalId);
159         }
160     }
161 
onExecuted(boolean needsReschedule)162     void onExecuted(boolean needsReschedule) {
163         Logger.get().debug(TAG, "onExecuted " + mWorkGenerationalId + ", " + needsReschedule);
164         cleanUp();
165         if (needsReschedule) {
166             // We need to reschedule the WorkSpec. WorkerWrapper may also call Scheduler.schedule()
167             // but given that we will only consider WorkSpecs that are eligible that it safe.
168             Intent reschedule = CommandHandler.createScheduleWorkIntent(mContext,
169                     mWorkGenerationalId);
170             mMainThreadExecutor.execute(
171                     new SystemAlarmDispatcher.AddRunnable(mDispatcher, reschedule, mStartId));
172         }
173 
174         if (mHasConstraints) {
175             // The WorkSpec had constraints. Once the execution of the worker is complete,
176             // we might need to disable constraint proxies which were previously enabled for
177             // this WorkSpec. Hence, trigger a constraints changed command.
178             Intent intent = CommandHandler.createConstraintsChangedIntent(mContext);
179             mMainThreadExecutor.execute(
180                     new SystemAlarmDispatcher.AddRunnable(mDispatcher, intent, mStartId));
181         }
182     }
183 
184     @Override
onTimeLimitExceeded(@onNull WorkGenerationalId id)185     public void onTimeLimitExceeded(@NonNull WorkGenerationalId id) {
186         Logger.get().debug(TAG, "Exceeded time limits on execution for " + id);
187         mSerialExecutor.execute(this::stopWork);
188     }
189 
190     @WorkerThread
handleProcessWork()191     void handleProcessWork() {
192         String workSpecId = mWorkGenerationalId.getWorkSpecId();
193         mWakeLock = WakeLocks.newWakeLock(mContext, workSpecId + " (" + mStartId + ")");
194         Logger.get().debug(TAG,
195                 "Acquiring wakelock " + mWakeLock + "for WorkSpec " + workSpecId);
196         mWakeLock.acquire();
197 
198         WorkSpec workSpec = mDispatcher.getWorkManager()
199                 .getWorkDatabase()
200                 .workSpecDao()
201                 .getWorkSpec(workSpecId);
202         // This should typically never happen. Cancelling work should remove alarms, but if an
203         // alarm has already fired, then fire a stop work request to remove the pending delay met
204         // command handler.
205         if (workSpec == null) {
206             mSerialExecutor.execute(this::stopWork);
207             return;
208         }
209 
210         // Keep track of whether the WorkSpec had constraints. This is useful for updating the
211         // state of constraint proxies when onExecuted().
212         mHasConstraints = workSpec.hasConstraints();
213 
214         if (!mHasConstraints) {
215             Logger.get().debug(TAG, "No constraints for " + workSpecId);
216             mSerialExecutor.execute(this::startWork);
217         } else {
218             // Allow tracker to report constraint changes
219             mJob = listen(mWorkConstraintsTracker, workSpec, mCoroutineDispatcher, this);
220         }
221     }
222 
stopWork()223     private void stopWork() {
224         // No need to release the wake locks here. The stopWork command will eventually call
225         // onExecuted() if there is a corresponding pending delay met command handler; which in
226         // turn calls cleanUp().
227         String workSpecId = mWorkGenerationalId.getWorkSpecId();
228         if (mCurrentState < STATE_STOP_REQUESTED) {
229             mCurrentState = STATE_STOP_REQUESTED;
230             Logger.get().debug(TAG, "Stopping work for WorkSpec " + workSpecId);
231             Intent stopWork = CommandHandler.createStopWorkIntent(mContext, mWorkGenerationalId);
232             mMainThreadExecutor.execute(
233                     new SystemAlarmDispatcher.AddRunnable(mDispatcher, stopWork, mStartId));
234             // There are cases where the work may not have been enqueued at all, and therefore
235             // the processor is completely unaware of such a workSpecId in which case a
236             // reschedule should not happen. For e.g. DELAY_MET when constraints are not met,
237             // should not result in a reschedule.
238             if (mDispatcher.getProcessor().isEnqueued(mWorkGenerationalId.getWorkSpecId())) {
239                 Logger.get().debug(TAG, "WorkSpec " + workSpecId + " needs to be rescheduled");
240                 Intent reschedule = CommandHandler.createScheduleWorkIntent(mContext,
241                         mWorkGenerationalId);
242                 mMainThreadExecutor.execute(
243                         new SystemAlarmDispatcher.AddRunnable(mDispatcher, reschedule, mStartId)
244                 );
245             } else {
246                 Logger.get().debug(TAG, "Processor does not have WorkSpec " + workSpecId
247                         + ". No need to reschedule");
248             }
249         } else {
250             Logger.get().debug(TAG, "Already stopped work for " + workSpecId);
251         }
252     }
253 
cleanUp()254     private void cleanUp() {
255         // cleanUp() may occur from one of 2 threads.
256         // * In the call to bgProcessor.startWork() returns false,
257         //   it probably means that the worker is already being processed
258         //   so we just need to call cleanUp to release wakelocks on the command processor thread.
259         // * It could also happen on the onExecutionCompleted() pass of the bgProcessor.
260         // To avoid calling mWakeLock.release() twice, we are synchronizing here.
261         synchronized (mLock) {
262             // clean up constraint trackers
263             if (mJob != null) {
264                 mJob.cancel(null);
265             }
266             // stop timers
267             mDispatcher.getWorkTimer().stopTimer(mWorkGenerationalId);
268 
269             // release wake locks
270             if (mWakeLock != null && mWakeLock.isHeld()) {
271                 Logger.get().debug(TAG, "Releasing wakelock " + mWakeLock
272                         + "for WorkSpec " + mWorkGenerationalId);
273                 mWakeLock.release();
274             }
275         }
276     }
277 }
278