1 /* 2 * Copyright 2017 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 package androidx.work.impl; 17 18 import static androidx.work.impl.foreground.SystemForegroundDispatcher.createStartForegroundIntent; 19 import static androidx.work.impl.foreground.SystemForegroundDispatcher.createStopForegroundIntent; 20 21 import android.content.Context; 22 import android.content.Intent; 23 import android.os.PowerManager; 24 25 import androidx.annotation.RestrictTo; 26 import androidx.core.content.ContextCompat; 27 import androidx.work.Configuration; 28 import androidx.work.ForegroundInfo; 29 import androidx.work.Logger; 30 import androidx.work.WorkerParameters; 31 import androidx.work.impl.foreground.ForegroundProcessor; 32 import androidx.work.impl.model.WorkGenerationalId; 33 import androidx.work.impl.model.WorkSpec; 34 import androidx.work.impl.utils.WakeLocks; 35 import androidx.work.impl.utils.taskexecutor.TaskExecutor; 36 37 import com.google.common.util.concurrent.ListenableFuture; 38 39 import org.jspecify.annotations.NonNull; 40 import org.jspecify.annotations.Nullable; 41 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.concurrent.ExecutionException; 49 50 /** 51 * A Processor can intelligently schedule and execute work on demand. 52 */ 53 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 54 public class Processor implements ForegroundProcessor { 55 private static final String TAG = Logger.tagWithPrefix("Processor"); 56 private static final String FOREGROUND_WAKELOCK_TAG = "ProcessorForegroundLck"; 57 58 private PowerManager.@Nullable WakeLock mForegroundLock; 59 60 private Context mAppContext; 61 private Configuration mConfiguration; 62 private TaskExecutor mWorkTaskExecutor; 63 private WorkDatabase mWorkDatabase; 64 private Map<String, WorkerWrapper> mForegroundWorkMap; 65 private Map<String, WorkerWrapper> mEnqueuedWorkMap; 66 // workSpecId to a Set<WorkRunId> 67 private Map<String, Set<StartStopToken>> mWorkRuns; 68 private Set<String> mCancelledIds; 69 70 private final List<ExecutionListener> mOuterListeners; 71 private final Object mLock; 72 Processor( @onNull Context appContext, @NonNull Configuration configuration, @NonNull TaskExecutor workTaskExecutor, @NonNull WorkDatabase workDatabase)73 public Processor( 74 @NonNull Context appContext, 75 @NonNull Configuration configuration, 76 @NonNull TaskExecutor workTaskExecutor, 77 @NonNull WorkDatabase workDatabase) { 78 mAppContext = appContext; 79 mConfiguration = configuration; 80 mWorkTaskExecutor = workTaskExecutor; 81 mWorkDatabase = workDatabase; 82 mEnqueuedWorkMap = new HashMap<>(); 83 mForegroundWorkMap = new HashMap<>(); 84 mCancelledIds = new HashSet<>(); 85 mOuterListeners = new ArrayList<>(); 86 mForegroundLock = null; 87 mLock = new Object(); 88 mWorkRuns = new HashMap<>(); 89 } 90 91 /** 92 * Starts a given unit of work in the background. 93 * 94 * @param id The work id to execute. 95 * @return {@code true} if the work was successfully enqueued for processing 96 */ startWork(@onNull StartStopToken id)97 public boolean startWork(@NonNull StartStopToken id) { 98 return startWork(id, null); 99 } 100 101 /** 102 * Starts a given unit of work in the background. 103 * 104 * @param startStopToken The work id to execute. 105 * @param runtimeExtras The {@link WorkerParameters.RuntimeExtras} for this work, if any. 106 * @return {@code true} if the work was successfully enqueued for processing 107 */ 108 @SuppressWarnings("ConstantConditions") startWork( @onNull StartStopToken startStopToken, WorkerParameters.@Nullable RuntimeExtras runtimeExtras)109 public boolean startWork( 110 @NonNull StartStopToken startStopToken, 111 WorkerParameters.@Nullable RuntimeExtras runtimeExtras) { 112 WorkGenerationalId id = startStopToken.getId(); 113 String workSpecId = id.getWorkSpecId(); 114 ArrayList<String> tags = new ArrayList<>(); 115 WorkSpec workSpec = mWorkDatabase.runInTransaction( 116 () -> { 117 tags.addAll(mWorkDatabase.workTagDao().getTagsForWorkSpecId(workSpecId)); 118 return mWorkDatabase.workSpecDao().getWorkSpec(workSpecId); 119 } 120 ); 121 if (workSpec == null) { 122 Logger.get().warning(TAG, "Didn't find WorkSpec for id " + id); 123 runOnExecuted(id, false); 124 return false; 125 } 126 WorkerWrapper workWrapper; 127 synchronized (mLock) { 128 // Work may get triggered multiple times if they have passing constraints 129 // and new work with those constraints are added. 130 if (isEnqueued(workSpecId)) { 131 // there must be another run if it is enqueued. 132 Set<StartStopToken> tokens = mWorkRuns.get(workSpecId); 133 StartStopToken previousRun = tokens.iterator().next(); 134 int previousRunGeneration = previousRun.getId().getGeneration(); 135 if (previousRunGeneration == id.getGeneration()) { 136 tokens.add(startStopToken); 137 Logger.get().debug(TAG, "Work " + id + " is already enqueued for processing"); 138 } else { 139 // Implementation detail. 140 // If previousRunGeneration > id.getGeneration(), then we don't have to do 141 // anything because newer generation is already running 142 // 143 // Case of previousRunGeneration < id.getGeneration(): 144 // it should happen only in the case of the periodic worker, 145 // so we let run a current Worker, and periodic worker will schedule 146 // next iteration with updated work spec. 147 runOnExecuted(id, false); 148 } 149 return false; 150 } 151 152 if (workSpec.getGeneration() != id.getGeneration()) { 153 // not the latest generation, so ignoring this start request, 154 // new request with newer generation should arrive shortly. 155 runOnExecuted(id, false); 156 return false; 157 } 158 workWrapper = 159 new WorkerWrapper.Builder( 160 mAppContext, 161 mConfiguration, 162 mWorkTaskExecutor, 163 this, 164 mWorkDatabase, 165 workSpec, 166 tags) 167 .withRuntimeExtras(runtimeExtras) 168 .build(); 169 ListenableFuture<Boolean> future = workWrapper.launch(); 170 future.addListener( 171 () -> { 172 boolean needsReschedule; 173 try { 174 needsReschedule = future.get(); 175 } catch (InterruptedException | ExecutionException e) { 176 // Should never really happen(?) 177 needsReschedule = true; 178 } 179 onExecuted(workWrapper, needsReschedule); 180 }, 181 mWorkTaskExecutor.getMainThreadExecutor()); 182 mEnqueuedWorkMap.put(workSpecId, workWrapper); 183 HashSet<StartStopToken> set = new HashSet<>(); 184 set.add(startStopToken); 185 mWorkRuns.put(workSpecId, set); 186 } 187 Logger.get().debug(TAG, getClass().getSimpleName() + ": processing " + id); 188 return true; 189 } 190 191 @Override startForeground(@onNull String workSpecId, @NonNull ForegroundInfo foregroundInfo)192 public void startForeground(@NonNull String workSpecId, 193 @NonNull ForegroundInfo foregroundInfo) { 194 synchronized (mLock) { 195 Logger.get().info(TAG, "Moving WorkSpec (" + workSpecId + ") to the foreground"); 196 WorkerWrapper wrapper = mEnqueuedWorkMap.remove(workSpecId); 197 if (wrapper != null) { 198 if (mForegroundLock == null) { 199 mForegroundLock = WakeLocks.newWakeLock(mAppContext, FOREGROUND_WAKELOCK_TAG); 200 mForegroundLock.acquire(); 201 } 202 mForegroundWorkMap.put(workSpecId, wrapper); 203 Intent intent = createStartForegroundIntent(mAppContext, 204 wrapper.getWorkGenerationalId(), foregroundInfo); 205 ContextCompat.startForegroundService(mAppContext, intent); 206 } 207 } 208 } 209 210 /** 211 * Stops a unit of work running in the context of a foreground service. 212 * 213 * @param token The work to stop 214 * @return {@code true} if the work was stopped successfully 215 */ stopForegroundWork(@onNull StartStopToken token, int reason)216 public boolean stopForegroundWork(@NonNull StartStopToken token, int reason) { 217 String id = token.getId().getWorkSpecId(); 218 WorkerWrapper wrapper; 219 synchronized (mLock) { 220 // TODO: race, we can cancel next run of the worker. 221 wrapper = cleanUpWorkerUnsafe(id); 222 } 223 // Move interrupt() outside the critical section. 224 // This is because calling interrupt() eventually calls ListenableWorker.onStopped() 225 // If onStopped() takes too long, there is a good chance this causes an ANR 226 // in Processor.onExecuted(). 227 return interrupt(id, wrapper, reason); 228 } 229 230 /** 231 * Stops a unit of work. 232 * 233 * @param runId The work id to stop 234 * @return {@code true} if the work was stopped successfully 235 */ stopWork(@onNull StartStopToken runId, int reason)236 public boolean stopWork(@NonNull StartStopToken runId, int reason) { 237 String id = runId.getId().getWorkSpecId(); 238 WorkerWrapper wrapper; 239 synchronized (mLock) { 240 if (mForegroundWorkMap.get(id) != null) { 241 Logger.get().debug(TAG, 242 "Ignored stopWork. WorkerWrapper " + id + " is in foreground"); 243 return false; 244 } 245 // Processor _only_ receives stopWork() requests from the schedulers that originally 246 // scheduled the work, and not others. This means others are still notified about 247 // completion, but we avoid a accidental "stops" and lot of redundant work when 248 // attempting to stop. 249 Set<StartStopToken> runs = mWorkRuns.get(id); 250 if (runs == null || !runs.contains(runId)) { 251 return false; 252 } 253 wrapper = cleanUpWorkerUnsafe(id); 254 } 255 // Move interrupt() outside the critical section. 256 // This is because calling interrupt() eventually calls ListenableWorker.onStopped() 257 // If onStopped() takes too long, there is a good chance this causes an ANR 258 // in Processor.onExecuted(). 259 return interrupt(id, wrapper, reason); 260 } 261 262 /** 263 * Stops a unit of work and marks it as cancelled. 264 * 265 * @param id The work id to stop and cancel 266 * @return {@code true} if the work was stopped successfully 267 */ stopAndCancelWork(@onNull String id, int reason)268 public boolean stopAndCancelWork(@NonNull String id, int reason) { 269 WorkerWrapper wrapper; 270 synchronized (mLock) { 271 Logger.get().debug(TAG, "Processor cancelling " + id); 272 mCancelledIds.add(id); 273 // Check if running in the context of a foreground service 274 wrapper = cleanUpWorkerUnsafe(id); 275 } 276 // Move interrupt() outside the critical section. 277 // This is because calling interrupt() eventually calls ListenableWorker.onStopped() 278 // If onStopped() takes too long, there is a good chance this causes an ANR 279 // in Processor.onExecuted(). 280 return interrupt(id, wrapper, reason); 281 } 282 283 /** 284 * Determines if the given {@code id} is marked as cancelled. 285 * 286 * @param id The work id to query 287 * @return {@code true} if the id has already been marked as cancelled 288 */ isCancelled(@onNull String id)289 public boolean isCancelled(@NonNull String id) { 290 synchronized (mLock) { 291 return mCancelledIds.contains(id); 292 } 293 } 294 295 /** 296 * @return {@code true} if the processor has work to process. 297 */ hasWork()298 public boolean hasWork() { 299 synchronized (mLock) { 300 return !(mEnqueuedWorkMap.isEmpty() 301 && mForegroundWorkMap.isEmpty()); 302 } 303 } 304 305 /** 306 * @param workSpecId The {@link androidx.work.impl.model.WorkSpec} id 307 * @return {@code true} if the id was enqueued in the processor. 308 */ isEnqueued(@onNull String workSpecId)309 public boolean isEnqueued(@NonNull String workSpecId) { 310 synchronized (mLock) { 311 return getWorkerWrapperUnsafe(workSpecId) != null; 312 } 313 } 314 315 /** 316 * Adds an {@link ExecutionListener} to track when work finishes. 317 * 318 * @param executionListener The {@link ExecutionListener} to add 319 */ addExecutionListener(@onNull ExecutionListener executionListener)320 public void addExecutionListener(@NonNull ExecutionListener executionListener) { 321 synchronized (mLock) { 322 mOuterListeners.add(executionListener); 323 } 324 } 325 326 /** 327 * Removes a tracked {@link ExecutionListener}. 328 * 329 * @param executionListener The {@link ExecutionListener} to remove 330 */ removeExecutionListener(@onNull ExecutionListener executionListener)331 public void removeExecutionListener(@NonNull ExecutionListener executionListener) { 332 synchronized (mLock) { 333 mOuterListeners.remove(executionListener); 334 } 335 } 336 onExecuted(@onNull WorkerWrapper wrapper, boolean needsReschedule)337 private void onExecuted(@NonNull WorkerWrapper wrapper, boolean needsReschedule) { 338 synchronized (mLock) { 339 WorkGenerationalId id = wrapper.getWorkGenerationalId(); 340 String workSpecId = id.getWorkSpecId(); 341 WorkerWrapper workerWrapper = getWorkerWrapperUnsafe(workSpecId); 342 // can be called for another generation, so we shouldn't remove it 343 if (workerWrapper == wrapper) { 344 cleanUpWorkerUnsafe(workSpecId); 345 } 346 Logger.get().debug(TAG, 347 getClass().getSimpleName() + " " + workSpecId 348 + " executed; reschedule = " + needsReschedule); 349 for (ExecutionListener executionListener : mOuterListeners) { 350 executionListener.onExecuted(id, needsReschedule); 351 } 352 } 353 } 354 getWorkerWrapperUnsafe(@onNull String workSpecId)355 private @Nullable WorkerWrapper getWorkerWrapperUnsafe(@NonNull String workSpecId) { 356 WorkerWrapper workerWrapper = mForegroundWorkMap.get(workSpecId); 357 if (workerWrapper == null) { 358 workerWrapper = mEnqueuedWorkMap.get(workSpecId); 359 } 360 return workerWrapper; 361 } 362 363 /** 364 * Returns a spec of the running worker by the given id 365 * 366 * @param workSpecId id of running worker 367 */ getRunningWorkSpec(@onNull String workSpecId)368 public @Nullable WorkSpec getRunningWorkSpec(@NonNull String workSpecId) { 369 synchronized (mLock) { 370 WorkerWrapper workerWrapper = getWorkerWrapperUnsafe(workSpecId); 371 if (workerWrapper != null) { 372 return workerWrapper.getWorkSpec(); 373 } else { 374 return null; 375 } 376 } 377 } 378 runOnExecuted(final @NonNull WorkGenerationalId id, boolean needsReschedule)379 private void runOnExecuted(final @NonNull WorkGenerationalId id, boolean needsReschedule) { 380 mWorkTaskExecutor.getMainThreadExecutor().execute( 381 () -> { 382 synchronized (mLock) { 383 for (ExecutionListener executionListener : mOuterListeners) { 384 executionListener.onExecuted(id, needsReschedule); 385 } 386 } 387 } 388 ); 389 } 390 stopForegroundService()391 private void stopForegroundService() { 392 synchronized (mLock) { 393 boolean hasForegroundWork = !mForegroundWorkMap.isEmpty(); 394 if (!hasForegroundWork) { 395 Intent intent = createStopForegroundIntent(mAppContext); 396 try { 397 // Wrapping this inside a try..catch, because there are bugs the platform 398 // that cause an IllegalStateException when an intent is dispatched to stop 399 // the foreground service that is running. 400 mAppContext.startService(intent); 401 } catch (Throwable throwable) { 402 Logger.get().error(TAG, "Unable to stop foreground service", throwable); 403 } 404 // Release wake lock if there is no more pending work. 405 if (mForegroundLock != null) { 406 mForegroundLock.release(); 407 mForegroundLock = null; 408 } 409 } 410 } 411 } 412 cleanUpWorkerUnsafe(@onNull String id)413 private @Nullable WorkerWrapper cleanUpWorkerUnsafe(@NonNull String id) { 414 WorkerWrapper wrapper = mForegroundWorkMap.remove(id); 415 boolean wasForeground = wrapper != null; 416 if (!wasForeground) { 417 wrapper = mEnqueuedWorkMap.remove(id); 418 } 419 mWorkRuns.remove(id); 420 if (wasForeground) { 421 stopForegroundService(); 422 } 423 return wrapper; 424 } 425 426 /** 427 * Interrupts a unit of work. 428 * 429 * @param id The {@link androidx.work.impl.model.WorkSpec} id 430 * @param wrapper The {@link WorkerWrapper} 431 * @return {@code true} if the work was stopped successfully 432 */ interrupt(@onNull String id, @Nullable WorkerWrapper wrapper, int stopReason)433 private static boolean interrupt(@NonNull String id, 434 @Nullable WorkerWrapper wrapper, int stopReason) { 435 if (wrapper != null) { 436 wrapper.interrupt(stopReason); 437 Logger.get().debug(TAG, "WorkerWrapper interrupted for " + id); 438 return true; 439 } else { 440 Logger.get().debug(TAG, "WorkerWrapper could not be found for " + id); 441 return false; 442 } 443 } 444 } 445