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