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