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 android.content.Context; 20 import android.content.Intent; 21 import android.os.Bundle; 22 23 import androidx.annotation.RestrictTo; 24 import androidx.annotation.WorkerThread; 25 import androidx.work.Clock; 26 import androidx.work.Logger; 27 import androidx.work.impl.ExecutionListener; 28 import androidx.work.impl.StartStopToken; 29 import androidx.work.impl.StartStopTokens; 30 import androidx.work.impl.WorkDatabase; 31 import androidx.work.impl.WorkManagerImpl; 32 import androidx.work.impl.model.WorkGenerationalId; 33 import androidx.work.impl.model.WorkSpec; 34 import androidx.work.impl.model.WorkSpecDao; 35 36 import org.jspecify.annotations.NonNull; 37 import org.jspecify.annotations.Nullable; 38 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 /** 45 * The command handler used by {@link SystemAlarmDispatcher}. 46 * 47 */ 48 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 49 public class CommandHandler implements ExecutionListener { 50 51 private static final String TAG = Logger.tagWithPrefix("CommandHandler"); 52 53 // actions 54 static final String ACTION_SCHEDULE_WORK = "ACTION_SCHEDULE_WORK"; 55 static final String ACTION_DELAY_MET = "ACTION_DELAY_MET"; 56 static final String ACTION_STOP_WORK = "ACTION_STOP_WORK"; 57 static final String ACTION_CONSTRAINTS_CHANGED = "ACTION_CONSTRAINTS_CHANGED"; 58 static final String ACTION_RESCHEDULE = "ACTION_RESCHEDULE"; 59 static final String ACTION_EXECUTION_COMPLETED = "ACTION_EXECUTION_COMPLETED"; 60 61 // keys 62 private static final String KEY_WORKSPEC_ID = "KEY_WORKSPEC_ID"; 63 private static final String KEY_WORKSPEC_GENERATION = "KEY_WORKSPEC_GENERATION"; 64 private static final String KEY_NEEDS_RESCHEDULE = "KEY_NEEDS_RESCHEDULE"; 65 66 // constants 67 static final long WORK_PROCESSING_TIME_IN_MS = 10 * 60 * 1000L; 68 69 // utilities createScheduleWorkIntent(@onNull Context context, @NonNull WorkGenerationalId id)70 static Intent createScheduleWorkIntent(@NonNull Context context, 71 @NonNull WorkGenerationalId id) { 72 Intent intent = new Intent(context, SystemAlarmService.class); 73 intent.setAction(ACTION_SCHEDULE_WORK); 74 return writeWorkGenerationalId(intent, id); 75 } 76 writeWorkGenerationalId(@onNull Intent intent, @NonNull WorkGenerationalId id)77 private static Intent writeWorkGenerationalId(@NonNull Intent intent, 78 @NonNull WorkGenerationalId id) { 79 intent.putExtra(KEY_WORKSPEC_ID, id.getWorkSpecId()); 80 intent.putExtra(KEY_WORKSPEC_GENERATION, id.getGeneration()); 81 return intent; 82 } 83 readWorkGenerationalId(@onNull Intent intent)84 static WorkGenerationalId readWorkGenerationalId(@NonNull Intent intent) { 85 return new WorkGenerationalId(intent.getStringExtra(KEY_WORKSPEC_ID), 86 intent.getIntExtra(KEY_WORKSPEC_GENERATION, 0)); 87 } 88 createDelayMetIntent(@onNull Context context, @NonNull WorkGenerationalId id)89 static Intent createDelayMetIntent(@NonNull Context context, @NonNull WorkGenerationalId id) { 90 Intent intent = new Intent(context, SystemAlarmService.class); 91 intent.setAction(ACTION_DELAY_MET); 92 return writeWorkGenerationalId(intent, id); 93 } 94 createStopWorkIntent(@onNull Context context, @NonNull String workSpecId)95 static Intent createStopWorkIntent(@NonNull Context context, @NonNull String workSpecId) { 96 Intent intent = new Intent(context, SystemAlarmService.class); 97 intent.setAction(ACTION_STOP_WORK); 98 intent.putExtra(KEY_WORKSPEC_ID, workSpecId); 99 return intent; 100 } createStopWorkIntent(@onNull Context context, @NonNull WorkGenerationalId id)101 static Intent createStopWorkIntent(@NonNull Context context, @NonNull WorkGenerationalId id) { 102 Intent intent = new Intent(context, SystemAlarmService.class); 103 intent.setAction(ACTION_STOP_WORK); 104 return writeWorkGenerationalId(intent, id); 105 } 106 createConstraintsChangedIntent(@onNull Context context)107 static Intent createConstraintsChangedIntent(@NonNull Context context) { 108 Intent intent = new Intent(context, SystemAlarmService.class); 109 intent.setAction(ACTION_CONSTRAINTS_CHANGED); 110 return intent; 111 } 112 createRescheduleIntent(@onNull Context context)113 static Intent createRescheduleIntent(@NonNull Context context) { 114 Intent intent = new Intent(context, SystemAlarmService.class); 115 intent.setAction(ACTION_RESCHEDULE); 116 return intent; 117 } 118 createExecutionCompletedIntent( @onNull Context context, @NonNull WorkGenerationalId id, boolean needsReschedule)119 static Intent createExecutionCompletedIntent( 120 @NonNull Context context, 121 @NonNull WorkGenerationalId id, 122 boolean needsReschedule) { 123 Intent intent = new Intent(context, SystemAlarmService.class); 124 intent.setAction(ACTION_EXECUTION_COMPLETED); 125 intent.putExtra(KEY_NEEDS_RESCHEDULE, needsReschedule); 126 return writeWorkGenerationalId(intent, id); 127 } 128 129 // members 130 private final Context mContext; 131 private final Map<WorkGenerationalId, DelayMetCommandHandler> mPendingDelayMet; 132 private final Object mLock; 133 private final Clock mClock; 134 private final StartStopTokens mStartStopTokens; 135 CommandHandler(@onNull Context context, Clock clock, @NonNull StartStopTokens startStopTokens)136 CommandHandler(@NonNull Context context, Clock clock, 137 @NonNull StartStopTokens startStopTokens) { 138 mContext = context; 139 mClock = clock; 140 mStartStopTokens = startStopTokens; 141 mPendingDelayMet = new HashMap<>(); 142 mLock = new Object(); 143 } 144 145 @Override onExecuted(@onNull WorkGenerationalId id, boolean needsReschedule)146 public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) { 147 synchronized (mLock) { 148 // This listener is only necessary for knowing when a pending work is complete. 149 // Delegate to the underlying execution listener itself. 150 DelayMetCommandHandler listener = mPendingDelayMet.remove(id); 151 mStartStopTokens.remove(id); 152 if (listener != null) { 153 listener.onExecuted(needsReschedule); 154 } 155 } 156 } 157 158 /** 159 * @return <code>true</code> if there is work pending. 160 */ hasPendingCommands()161 boolean hasPendingCommands() { 162 // Needs to be synchronized as this could be checked from 163 // both the command processing thread, as well as the 164 // onExecuted callback. 165 synchronized (mLock) { 166 // If we have pending work being executed on the background 167 // processor - we are not done yet. 168 return !mPendingDelayMet.isEmpty(); 169 } 170 } 171 172 /** 173 * The actual command handler. 174 */ 175 @WorkerThread onHandleIntent( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)176 void onHandleIntent( 177 @NonNull Intent intent, 178 int startId, 179 @NonNull SystemAlarmDispatcher dispatcher) { 180 181 String action = intent.getAction(); 182 183 if (ACTION_CONSTRAINTS_CHANGED.equals(action)) { 184 handleConstraintsChanged(intent, startId, dispatcher); 185 } else if (ACTION_RESCHEDULE.equals(action)) { 186 handleReschedule(intent, startId, dispatcher); 187 } else { 188 Bundle extras = intent.getExtras(); 189 if (!hasKeys(extras, KEY_WORKSPEC_ID)) { 190 Logger.get().error(TAG, 191 "Invalid request for " + action + " , requires " + KEY_WORKSPEC_ID + " ."); 192 } else { 193 if (ACTION_SCHEDULE_WORK.equals(action)) { 194 handleScheduleWorkIntent(intent, startId, dispatcher); 195 } else if (ACTION_DELAY_MET.equals(action)) { 196 handleDelayMet(intent, startId, dispatcher); 197 } else if (ACTION_STOP_WORK.equals(action)) { 198 handleStopWork(intent, dispatcher); 199 } else if (ACTION_EXECUTION_COMPLETED.equals(action)) { 200 handleExecutionCompleted(intent, startId); 201 } else { 202 Logger.get().warning(TAG, "Ignoring intent " + intent); 203 } 204 } 205 } 206 } 207 handleScheduleWorkIntent( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)208 private void handleScheduleWorkIntent( 209 @NonNull Intent intent, 210 int startId, 211 @NonNull SystemAlarmDispatcher dispatcher) { 212 213 WorkGenerationalId id = readWorkGenerationalId(intent); 214 Logger.get().debug(TAG, "Handling schedule work for " + id); 215 216 WorkManagerImpl workManager = dispatcher.getWorkManager(); 217 WorkDatabase workDatabase = workManager.getWorkDatabase(); 218 workDatabase.beginTransaction(); 219 220 try { 221 WorkSpecDao workSpecDao = workDatabase.workSpecDao(); 222 WorkSpec workSpec = workSpecDao.getWorkSpec(id.getWorkSpecId()); 223 224 // It is possible that this WorkSpec got cancelled/pruned since this isn't part of 225 // the same database transaction as marking it enqueued (for example, if we using 226 // any of the synchronous operations). For now, handle this gracefully by exiting 227 // the loop. When we plumb ListenableFutures all the way through, we can remove the 228 // *sync methods and return ListenableFutures, which will block on an operation on 229 // the background task thread so all database operations happen on the same thread. 230 // See b/114705286. 231 if (workSpec == null) { 232 Logger.get().warning(TAG, 233 "Skipping scheduling " + id + " because it's no longer in " 234 + "the DB"); 235 return; 236 } else if (workSpec.state.isFinished()) { 237 // We need to schedule the Alarms, even when the Worker is RUNNING. This is because 238 // if the process gets killed, the Alarm is necessary to pick up the execution of 239 // Work. 240 Logger.get().warning(TAG, 241 "Skipping scheduling " + id + "because it is finished."); 242 return; 243 } 244 245 // Note: The first instance of PeriodicWorker getting scheduled will set an alarm in the 246 // past. This is because periodStartTime = 0. 247 long triggerAt = workSpec.calculateNextRunTime(); 248 249 if (!workSpec.hasConstraints()) { 250 Logger.get().debug(TAG, 251 "Setting up Alarms for " + id + "at " + triggerAt); 252 Alarms.setAlarm(mContext, workDatabase, id, triggerAt); 253 } else { 254 // Schedule an alarm irrespective of whether all constraints matched. 255 Logger.get().debug(TAG, 256 "Opportunistically setting an alarm for " + id + "at " + triggerAt); 257 Alarms.setAlarm( 258 mContext, 259 workDatabase, 260 id, 261 triggerAt); 262 263 // Schedule an update for constraint proxies 264 // This in turn sets enables us to track changes in constraints 265 Intent constraintsUpdate = CommandHandler.createConstraintsChangedIntent(mContext); 266 dispatcher.getTaskExecutor().getMainThreadExecutor().execute( 267 new SystemAlarmDispatcher.AddRunnable( 268 dispatcher, 269 constraintsUpdate, 270 startId)); 271 } 272 273 workDatabase.setTransactionSuccessful(); 274 } finally { 275 workDatabase.endTransaction(); 276 } 277 } 278 handleDelayMet( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)279 private void handleDelayMet( 280 @NonNull Intent intent, 281 int startId, 282 @NonNull SystemAlarmDispatcher dispatcher) { 283 284 synchronized (mLock) { 285 WorkGenerationalId id = readWorkGenerationalId(intent); 286 Logger.get().debug(TAG, "Handing delay met for " + id); 287 288 // Check to see if we are already handling an ACTION_DELAY_MET for the WorkSpec. 289 // If we are, then there is nothing for us to do. 290 if (!mPendingDelayMet.containsKey(id)) { 291 DelayMetCommandHandler delayMetCommandHandler = 292 new DelayMetCommandHandler(mContext, startId, 293 dispatcher, mStartStopTokens.tokenFor(id)); 294 mPendingDelayMet.put(id, delayMetCommandHandler); 295 delayMetCommandHandler.handleProcessWork(); 296 } else { 297 Logger.get().debug(TAG, "WorkSpec " + id 298 + " is is already being handled for ACTION_DELAY_MET"); 299 } 300 } 301 } 302 handleStopWork( @onNull Intent intent, @NonNull SystemAlarmDispatcher dispatcher)303 private void handleStopWork( 304 @NonNull Intent intent, 305 @NonNull SystemAlarmDispatcher dispatcher) { 306 307 Bundle extras = intent.getExtras(); 308 String workSpecId = extras.getString(KEY_WORKSPEC_ID); 309 List<StartStopToken> tokens; 310 if (extras.containsKey(KEY_WORKSPEC_GENERATION)) { 311 int generation = extras.getInt(KEY_WORKSPEC_GENERATION); 312 tokens = new ArrayList<>(1); 313 StartStopToken id = mStartStopTokens.remove( 314 new WorkGenerationalId(workSpecId, generation)); 315 if (id != null) { 316 tokens.add(id); 317 } 318 } else { 319 tokens = mStartStopTokens.remove(workSpecId); 320 } 321 for (StartStopToken token: tokens) { 322 Logger.get().debug(TAG, "Handing stopWork work for " + workSpecId); 323 dispatcher.getWorkerLauncher().stopWork(token); 324 Alarms.cancelAlarm(mContext, 325 dispatcher.getWorkManager().getWorkDatabase(), token.getId()); 326 327 // Notify dispatcher, so it can clean up. 328 dispatcher.onExecuted(token.getId(), false /* never reschedule */); 329 } 330 } 331 handleConstraintsChanged( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)332 private void handleConstraintsChanged( 333 @NonNull Intent intent, int startId, 334 @NonNull SystemAlarmDispatcher dispatcher) { 335 336 Logger.get().debug(TAG, "Handling constraints changed " + intent); 337 // Constraints changed command handler is synchronous. No cleanup 338 // is necessary. 339 ConstraintsCommandHandler changedCommandHandler = 340 new ConstraintsCommandHandler(mContext, mClock, startId, dispatcher); 341 changedCommandHandler.handleConstraintsChanged(); 342 } 343 handleReschedule( @onNull Intent intent, int startId, @NonNull SystemAlarmDispatcher dispatcher)344 private void handleReschedule( 345 @NonNull Intent intent, 346 int startId, 347 @NonNull SystemAlarmDispatcher dispatcher) { 348 349 Logger.get().debug(TAG, "Handling reschedule " + intent + ", " + startId); 350 dispatcher.getWorkManager().rescheduleEligibleWork(); 351 } 352 handleExecutionCompleted( @onNull Intent intent, int startId)353 private void handleExecutionCompleted( 354 @NonNull Intent intent, 355 int startId) { 356 WorkGenerationalId id = readWorkGenerationalId(intent); 357 boolean needsReschedule = intent.getExtras().getBoolean(KEY_NEEDS_RESCHEDULE); 358 Logger.get().debug( 359 TAG, 360 "Handling onExecutionCompleted " + intent + ", " + startId); 361 // Delegate onExecuted() to the command handler. 362 onExecuted(id, needsReschedule); 363 } 364 365 @SuppressWarnings("deprecation") hasKeys(@ullable Bundle bundle, String @NonNull ... keys)366 private static boolean hasKeys(@Nullable Bundle bundle, String @NonNull ... keys) { 367 if (bundle == null || bundle.isEmpty()) { 368 return false; 369 } else { 370 for (String key : keys) { 371 if (bundle.get(key) == null) { 372 return false; 373 } 374 } 375 return true; 376 } 377 } 378 } 379