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