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.Looper;
22 import android.os.PowerManager;
23 import android.text.TextUtils;
24 
25 import androidx.annotation.MainThread;
26 import androidx.annotation.RestrictTo;
27 import androidx.annotation.VisibleForTesting;
28 import androidx.work.Logger;
29 import androidx.work.impl.ExecutionListener;
30 import androidx.work.impl.Processor;
31 import androidx.work.impl.StartStopTokens;
32 import androidx.work.impl.WorkLauncher;
33 import androidx.work.impl.WorkLauncherImpl;
34 import androidx.work.impl.WorkManagerImpl;
35 import androidx.work.impl.model.WorkGenerationalId;
36 import androidx.work.impl.utils.WakeLocks;
37 import androidx.work.impl.utils.WorkTimer;
38 import androidx.work.impl.utils.taskexecutor.SerialExecutor;
39 import androidx.work.impl.utils.taskexecutor.TaskExecutor;
40 
41 import org.jspecify.annotations.NonNull;
42 import org.jspecify.annotations.Nullable;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 
47 /**
48  * The dispatcher used by the background processor which is based on
49  * {@link android.app.AlarmManager}.
50  *
51  */
52 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
53 public class SystemAlarmDispatcher implements ExecutionListener {
54 
55     // Synthetic accessor
56     static final String TAG = Logger.tagWithPrefix("SystemAlarmDispatcher");
57 
58     private static final String PROCESS_COMMAND_TAG = "ProcessCommand";
59     private static final String KEY_START_ID = "KEY_START_ID";
60     private static final int DEFAULT_START_ID = 0;
61 
62     @SuppressWarnings("WeakerAccess") /* synthetic access */
63     final Context mContext;
64     @SuppressWarnings("WeakerAccess") /* synthetic access */
65     final TaskExecutor mTaskExecutor;
66     private final WorkTimer mWorkTimer;
67     private final Processor mProcessor;
68     private final WorkManagerImpl mWorkManager;
69     @SuppressWarnings("WeakerAccess") /* synthetic access */
70     final CommandHandler mCommandHandler;
71     @SuppressWarnings("WeakerAccess") /* synthetic access */
72     final List<Intent> mIntents;
73     Intent mCurrentIntent;
74 
75     private @Nullable CommandsCompletedListener mCompletedListener;
76 
77     private StartStopTokens mStartStopTokens;
78     private final WorkLauncher mWorkLauncher;
79 
SystemAlarmDispatcher(@onNull Context context)80     SystemAlarmDispatcher(@NonNull Context context) {
81         this(context, null, null, null);
82     }
83 
84     @VisibleForTesting
SystemAlarmDispatcher( @onNull Context context, @Nullable Processor processor, @Nullable WorkManagerImpl workManager, @Nullable WorkLauncher launcher )85     SystemAlarmDispatcher(
86             @NonNull Context context,
87             @Nullable Processor processor,
88             @Nullable WorkManagerImpl workManager,
89             @Nullable WorkLauncher launcher
90     ) {
91         mContext = context.getApplicationContext();
92         mStartStopTokens = StartStopTokens.create();
93         mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance(context);
94         mCommandHandler = new CommandHandler(
95                 mContext, mWorkManager.getConfiguration().getClock(), mStartStopTokens);
96         mWorkTimer = new WorkTimer(mWorkManager.getConfiguration().getRunnableScheduler());
97         mProcessor = processor != null ? processor : mWorkManager.getProcessor();
98         mTaskExecutor = mWorkManager.getWorkTaskExecutor();
99         mWorkLauncher = launcher != null ? launcher :
100                 new WorkLauncherImpl(mProcessor, mTaskExecutor);
101         mProcessor.addExecutionListener(this);
102         // a list of pending intents which need to be processed
103         mIntents = new ArrayList<>();
104         // the current intent (command) being processed.
105         mCurrentIntent = null;
106     }
107 
108     /**
109      * This method needs to be idempotent. This could be called more than once, and therefore,
110      * this method should only perform cleanup when necessary.
111      */
onDestroy()112     void onDestroy() {
113         Logger.get().debug(TAG, "Destroying SystemAlarmDispatcher");
114         mProcessor.removeExecutionListener(this);
115         mCompletedListener = null;
116     }
117 
118     @Override
onExecuted(@onNull WorkGenerationalId id, boolean needsReschedule)119     public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) {
120 
121         // When there are lots of workers completing at around the same time,
122         // this creates lock contention for the DelayMetCommandHandlers inside the CommandHandler.
123         // So move the actual execution of the post completion callbacks on the command executor
124         // thread.
125         mTaskExecutor.getMainThreadExecutor().execute(
126                 new AddRunnable(
127                         this,
128                         CommandHandler.createExecutionCompletedIntent(
129                                 mContext,
130                                 id,
131                                 needsReschedule),
132                         DEFAULT_START_ID));
133     }
134 
135     /**
136      * Adds the {@link Intent} intent and the startId to the command processor queue.
137      *
138      * @param intent  The {@link Intent} command that needs to be added to the command queue.
139      * @param startId The command startId
140      * @return <code>true</code> when the command was added to the command processor queue.
141      */
142     @MainThread
add(final @NonNull Intent intent, final int startId)143     public boolean add(final @NonNull Intent intent, final int startId) {
144         Logger.get().debug(TAG, "Adding command " + intent + " (" + startId + ")");
145         assertMainThread();
146         String action = intent.getAction();
147         if (TextUtils.isEmpty(action)) {
148             Logger.get().warning(TAG, "Unknown command. Ignoring");
149             return false;
150         }
151 
152         // If we have a constraints changed intent in the queue don't add a second one. We are
153         // treating this intent as special because every time a worker with constraints is complete
154         // it kicks off an update for constraint proxies.
155         if (CommandHandler.ACTION_CONSTRAINTS_CHANGED.equals(action)
156                 && hasIntentWithAction(CommandHandler.ACTION_CONSTRAINTS_CHANGED)) {
157             return false;
158         }
159 
160         intent.putExtra(KEY_START_ID, startId);
161         synchronized (mIntents) {
162             boolean hasCommands = !mIntents.isEmpty();
163             mIntents.add(intent);
164             if (!hasCommands) {
165                 // Only call processCommand if this is the first command.
166                 // The call to dequeueAndCheckForCompletion will process the remaining commands
167                 // in the order that they were added.
168                 processCommand();
169             }
170         }
171         return true;
172     }
173 
setCompletedListener(@onNull CommandsCompletedListener listener)174     void setCompletedListener(@NonNull CommandsCompletedListener listener) {
175         if (mCompletedListener != null) {
176             Logger.get().error(
177                     TAG,
178                     "A completion listener for SystemAlarmDispatcher already exists.");
179             return;
180         }
181         mCompletedListener = listener;
182     }
183 
getProcessor()184     Processor getProcessor() {
185         return mProcessor;
186     }
187 
getWorkTimer()188     WorkTimer getWorkTimer() {
189         return mWorkTimer;
190     }
191 
getWorkManager()192     WorkManagerImpl getWorkManager() {
193         return mWorkManager;
194     }
195 
getTaskExecutor()196     TaskExecutor getTaskExecutor() {
197         return mTaskExecutor;
198     }
199 
getWorkerLauncher()200     WorkLauncher getWorkerLauncher() {
201         return mWorkLauncher;
202     }
203 
204     @MainThread
205     @SuppressWarnings("WeakerAccess") /* synthetic access */
dequeueAndCheckForCompletion()206     void dequeueAndCheckForCompletion() {
207         Logger.get().debug(TAG, "Checking if commands are complete.");
208         assertMainThread();
209 
210         synchronized (mIntents) {
211             // Remove the intent from the list of processed commands.
212             // We are doing this to avoid a race condition between completion of a
213             // command in the command handler, and the checkForCompletion triggered
214             // by a worker's onExecutionComplete().
215             // For e.g.
216             // t0 -> delay_met_intent
217             // t1 -> bgProcessor.startWork(workSpec)
218             // t2 -> constraints_changed_intent
219             // t3 -> bgProcessor.onExecutionCompleted(...)
220             // t4 -> DequeueAndCheckForCompletion (while constraints_changed_intent is
221             // still being processed).
222 
223             // Note: this works only because mCommandExecutor service is a single
224             // threaded executor. If that assumption changes in the future, use a
225             // ReentrantLock, and lock the queue while command processor processes
226             // an intent. Synchronized to prevent ConcurrentModificationExceptions.
227             if (mCurrentIntent != null) {
228                 Logger.get().debug(TAG, "Removing command " + mCurrentIntent);
229                 if (!mIntents.remove(0).equals(mCurrentIntent)) {
230                     throw new IllegalStateException("Dequeue-d command is not the first.");
231                 }
232                 mCurrentIntent = null;
233             }
234             SerialExecutor serialExecutor = mTaskExecutor.getSerialTaskExecutor();
235             if (!mCommandHandler.hasPendingCommands()
236                     && mIntents.isEmpty()
237                     && !serialExecutor.hasPendingTasks()) {
238 
239                 // If there are no more intents to process, and the command handler
240                 // has no more pending commands, stop the service.
241                 Logger.get().debug(TAG, "No more commands & intents.");
242                 if (mCompletedListener != null) {
243                     mCompletedListener.onAllCommandsCompleted();
244                 }
245             } else if (!mIntents.isEmpty()) {
246                 // Only process the next command if we have more commands.
247                 processCommand();
248             }
249         }
250     }
251 
252     @MainThread
253     @SuppressWarnings("FutureReturnValueIgnored")
processCommand()254     private void processCommand() {
255         assertMainThread();
256         PowerManager.WakeLock processCommandLock =
257                 WakeLocks.newWakeLock(mContext, PROCESS_COMMAND_TAG);
258         try {
259             processCommandLock.acquire();
260             // Process commands on the background thread.
261             mWorkManager.getWorkTaskExecutor().executeOnTaskThread(new Runnable() {
262                 @Override
263                 public void run() {
264                     synchronized (mIntents) {
265                         mCurrentIntent = mIntents.get(0);
266                     }
267 
268                     if (mCurrentIntent != null) {
269                         final String action = mCurrentIntent.getAction();
270                         final int startId = mCurrentIntent.getIntExtra(KEY_START_ID,
271                                 DEFAULT_START_ID);
272                         Logger.get().debug(TAG,
273                                 "Processing command " + mCurrentIntent + ", " + startId);
274                         final PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(
275                                 mContext,
276                                 action + " (" + startId + ")");
277                         try {
278                             Logger.get().debug(TAG,
279                                     "Acquiring operation wake lock (" + action + ") " + wakeLock);
280                             wakeLock.acquire();
281                             mCommandHandler.onHandleIntent(mCurrentIntent, startId,
282                                     SystemAlarmDispatcher.this);
283                         } catch (Throwable throwable) {
284                             Logger.get().error(
285                                     TAG,
286                                     "Unexpected error in onHandleIntent",
287                                     throwable);
288                         }  finally {
289                             Logger.get().debug(
290                                     TAG,
291                                     "Releasing operation wake lock (" + action + ") " + wakeLock);
292                             wakeLock.release();
293                             // Check if we have processed all commands
294                             mTaskExecutor.getMainThreadExecutor().execute(
295                                     new DequeueAndCheckForCompletion(SystemAlarmDispatcher.this)
296                             );
297                         }
298                     }
299                 }
300             });
301         } finally {
302             processCommandLock.release();
303         }
304     }
305 
306     @MainThread
hasIntentWithAction(@onNull String action)307     private boolean hasIntentWithAction(@NonNull String action) {
308         assertMainThread();
309         synchronized (mIntents) {
310             for (Intent intent : mIntents) {
311                 if (action.equals(intent.getAction())) {
312                     return true;
313                 }
314             }
315             return false;
316         }
317     }
318 
assertMainThread()319     private void assertMainThread() {
320         if (Looper.getMainLooper().getThread() != Thread.currentThread()) {
321             throw new IllegalStateException("Needs to be invoked on the main thread.");
322         }
323     }
324 
325     /**
326      * Checks if we are done executing all commands after dequeue-ing the current command.
327      */
328     static class DequeueAndCheckForCompletion implements Runnable {
329         private final SystemAlarmDispatcher mDispatcher;
330 
DequeueAndCheckForCompletion(@onNull SystemAlarmDispatcher dispatcher)331         DequeueAndCheckForCompletion(@NonNull SystemAlarmDispatcher dispatcher) {
332             mDispatcher = dispatcher;
333         }
334 
335         @Override
run()336         public void run() {
337             mDispatcher.dequeueAndCheckForCompletion();
338         }
339     }
340 
341     /**
342      * Adds a new intent to the SystemAlarmDispatcher.
343      */
344     static class AddRunnable implements Runnable {
345         private final SystemAlarmDispatcher mDispatcher;
346         private final Intent mIntent;
347         private final int mStartId;
348 
AddRunnable(@onNull SystemAlarmDispatcher dispatcher, @NonNull Intent intent, int startId)349         AddRunnable(@NonNull SystemAlarmDispatcher dispatcher,
350                 @NonNull Intent intent,
351                 int startId) {
352             mDispatcher = dispatcher;
353             mIntent = intent;
354             mStartId = startId;
355         }
356 
357         @Override
run()358         public void run() {
359             mDispatcher.add(mIntent, mStartId);
360         }
361     }
362 
363     /**
364      * Used to notify interested parties when all pending commands and work is complete.
365      */
366     interface CommandsCompletedListener {
onAllCommandsCompleted()367         void onAllCommandsCompleted();
368     }
369 }
370