• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 com.android.voicemail.impl.scheduling;
18 
19 import android.annotation.TargetApi;
20 import android.app.Service;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.os.Binder;
24 import android.os.Build.VERSION_CODES;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.HandlerThread;
28 import android.os.IBinder;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.support.annotation.MainThread;
32 import android.support.annotation.Nullable;
33 import android.support.annotation.VisibleForTesting;
34 import android.support.annotation.WorkerThread;
35 import com.android.voicemail.impl.Assert;
36 import com.android.voicemail.impl.NeededForTesting;
37 import com.android.voicemail.impl.VvmLog;
38 import com.android.voicemail.impl.scheduling.TaskQueue.NextTask;
39 import java.util.List;
40 
41 /**
42  * A service to queue and run {@link Task} with the {@link android.app.job.JobScheduler}. A task is
43  * queued using {@link Context#startService(Intent)}. The intent should contain enough information
44  * in {@link Intent#getExtras()} to construct the task (see {@link Tasks#createIntent(Context,
45  * Class)}).
46  *
47  * <p>All tasks are ran in the background with a wakelock being held by the {@link
48  * android.app.job.JobScheduler}, which is between {@link #onStartJob(Job, List)} and {@link
49  * #finishJob()}. The {@link TaskSchedulerJobService} also has a {@link TaskQueue}, but the data is
50  * stored in the {@link android.app.job.JobScheduler} instead of the process memory, so if the
51  * process is killed the queued tasks will be restored. If a new task is added, a new {@link
52  * TaskSchedulerJobService} will be scheduled to run the task. If the job is already scheduled, the
53  * new task will be pushed into the queue of the scheduled job. If the job is already running, the
54  * job will be queued in process memory.
55  *
56  * <p>Only one task will be ran at a time, and same task cannot exist in the queue at the same time.
57  * Refer to {@link TaskQueue} for queuing and execution order.
58  *
59  * <p>If there are still tasks in the queue but none are executable immediately, the service will
60  * enter a "sleep", pushing all remaining task into a new job and end the current job.
61  *
62  * <p>The service will be started when a intent is received, and stopped when there are no more
63  * tasks in the queue.
64  *
65  * <p>{@link android.app.job.JobScheduler} is not used directly due to:
66  *
67  * <ul>
68  *   <li>The {@link android.telecom.PhoneAccountHandle} used to differentiate task can not be easily
69  *       mapped into an integer for job id
70  *   <li>A job cannot be mutated to store information such as retry count.
71  * </ul>
72  */
73 @SuppressWarnings("AndroidApiChecker") /* stream() */
74 @TargetApi(VERSION_CODES.O)
75 public class TaskSchedulerService extends Service {
76 
77   interface Job {
finish()78     void finish();
79   }
80 
81   private static final String TAG = "VvmTaskScheduler";
82 
83   private static final int READY_TOLERANCE_MILLISECONDS = 100;
84 
85   /**
86    * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
87    * future.
88    *
89    * <p>A short sleep will continue the job and use {@link Handler#postDelayed(Runnable, long)} to
90    * wait for the next task.
91    *
92    * <p>A long sleep will finish the job and schedule a new one. The exact execution time is
93    * subjected to {@link android.app.job.JobScheduler} battery optimization, and is not exact.
94    */
95   private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 10_000;
96   /**
97    * When there are no more tasks to be run the service should be stopped. But when all tasks has
98    * finished there might still be more tasks in the message queue waiting to be processed,
99    * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
100    * the service to make sure there are no pending messages.
101    */
102   private static final int STOP_DELAY_MILLISECONDS = 5_000;
103 
104   // The thread to run tasks on
105   private volatile WorkerThreadHandler mWorkerThreadHandler;
106 
107   /**
108    * Used by tests to turn task handling into a single threaded process by calling {@link
109    * Handler#handleMessage(Message)} directly
110    */
111   private MessageSender mMessageSender = new MessageSender();
112 
113   private MainThreadHandler mMainThreadHandler;
114 
115   // Binder given to clients
116   private final IBinder mBinder = new LocalBinder();
117 
118   /** Main thread only, access through {@link #getTasks()} */
119   private final TaskQueue mTasks = new TaskQueue();
120 
121   private boolean mWorkerThreadIsBusy = false;
122 
123   private Job mJob;
124 
125   private final Runnable mStopServiceWithDelay =
126       new Runnable() {
127         @MainThread
128         @Override
129         public void run() {
130           VvmLog.i(TAG, "Stopping service");
131           finishJob();
132           stopSelf();
133         }
134       };
135 
136   /** Should attempt to run the next task when a task has finished or been added. */
137   private boolean mTaskAutoRunDisabledForTesting = false;
138 
139   @VisibleForTesting
140   final class WorkerThreadHandler extends Handler {
141 
WorkerThreadHandler(Looper looper)142     public WorkerThreadHandler(Looper looper) {
143       super(looper);
144     }
145 
146     @Override
147     @WorkerThread
handleMessage(Message msg)148     public void handleMessage(Message msg) {
149       Assert.isNotMainThread();
150       Task task = (Task) msg.obj;
151       try {
152         VvmLog.i(TAG, "executing task " + task);
153         task.onExecuteInBackgroundThread();
154       } catch (Throwable throwable) {
155         VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
156       }
157 
158       Message schedulerMessage = mMainThreadHandler.obtainMessage();
159       schedulerMessage.obj = task;
160       mMessageSender.send(schedulerMessage);
161     }
162   }
163 
164   @VisibleForTesting
165   final class MainThreadHandler extends Handler {
166 
MainThreadHandler(Looper looper)167     public MainThreadHandler(Looper looper) {
168       super(looper);
169     }
170 
171     @Override
172     @MainThread
handleMessage(Message msg)173     public void handleMessage(Message msg) {
174       Assert.isMainThread();
175       Task task = (Task) msg.obj;
176       getTasks().remove(task);
177       task.onCompleted();
178       mWorkerThreadIsBusy = false;
179       maybeRunNextTask();
180     }
181   }
182 
183   @Override
184   @MainThread
onCreate()185   public void onCreate() {
186     super.onCreate();
187     HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
188     thread.start();
189 
190     mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
191     mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
192   }
193 
194   @Override
onDestroy()195   public void onDestroy() {
196     mWorkerThreadHandler.getLooper().quit();
197   }
198 
199   @Override
200   @MainThread
onStartCommand(@ullable Intent intent, int flags, int startId)201   public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
202     Assert.isMainThread();
203     if (intent == null) {
204       VvmLog.w(TAG, "null intent received");
205       return START_NOT_STICKY;
206     }
207     Task task = Tasks.createTask(this, intent.getExtras());
208     Assert.isTrue(task != null);
209     addTask(task);
210 
211     mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
212     VvmLog.i(TAG, "task added");
213     if (mJob == null) {
214       scheduleJob(0, true);
215     } else {
216       maybeRunNextTask();
217     }
218     // STICKY means the service will be automatically restarted will the last intent if it is
219     // killed.
220     return START_NOT_STICKY;
221   }
222 
223   @MainThread
224   @VisibleForTesting
addTask(Task task)225   void addTask(Task task) {
226     Assert.isMainThread();
227     getTasks().add(task);
228   }
229 
230   @MainThread
231   @VisibleForTesting
getTasks()232   TaskQueue getTasks() {
233     Assert.isMainThread();
234     return mTasks;
235   }
236 
237   @MainThread
maybeRunNextTask()238   private void maybeRunNextTask() {
239     Assert.isMainThread();
240     if (mWorkerThreadIsBusy) {
241       return;
242     }
243     if (mTaskAutoRunDisabledForTesting) {
244       // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
245       // to run the next task.
246       return;
247     }
248 
249     runNextTask();
250   }
251 
252   @VisibleForTesting
253   @MainThread
runNextTask()254   void runNextTask() {
255     Assert.isMainThread();
256     if (getTasks().isEmpty()) {
257       prepareStop();
258       return;
259     }
260     NextTask nextTask = getTasks().getNextTask(READY_TOLERANCE_MILLISECONDS);
261 
262     if (nextTask.task != null) {
263       nextTask.task.onBeforeExecute();
264       Message message = mWorkerThreadHandler.obtainMessage();
265       message.obj = nextTask.task;
266       mWorkerThreadIsBusy = true;
267       mMessageSender.send(message);
268       return;
269     }
270     VvmLog.i(TAG, "minimal wait time:" + nextTask.minimalWaitTimeMillis);
271     if (!mTaskAutoRunDisabledForTesting && nextTask.minimalWaitTimeMillis != null) {
272       // No tasks are currently ready. Sleep until the next one should be.
273       // If a new task is added during the sleep the service will wake immediately.
274       sleep(nextTask.minimalWaitTimeMillis);
275     }
276   }
277 
278   @MainThread
sleep(long timeMillis)279   private void sleep(long timeMillis) {
280     VvmLog.i(TAG, "sleep for " + timeMillis + " millis");
281     if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
282       mMainThreadHandler.postDelayed(
283           new Runnable() {
284             @Override
285             public void run() {
286               maybeRunNextTask();
287             }
288           },
289           timeMillis);
290       return;
291     }
292     finishJob();
293     mMainThreadHandler.post(() -> scheduleJob(timeMillis, false));
294   }
295 
serializePendingTasks()296   private List<Bundle> serializePendingTasks() {
297     return getTasks().toBundles();
298   }
299 
prepareStop()300   private void prepareStop() {
301     VvmLog.i(
302         TAG,
303         "no more tasks, stopping service if no task are added in "
304             + STOP_DELAY_MILLISECONDS
305             + " millis");
306     mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
307   }
308 
309   @NeededForTesting
310   static class MessageSender {
311 
send(Message message)312     public void send(Message message) {
313       message.sendToTarget();
314     }
315   }
316 
317   @NeededForTesting
setTaskAutoRunDisabledForTest(boolean value)318   void setTaskAutoRunDisabledForTest(boolean value) {
319     mTaskAutoRunDisabledForTesting = value;
320   }
321 
322   @NeededForTesting
setMessageSenderForTest(MessageSender sender)323   void setMessageSenderForTest(MessageSender sender) {
324     mMessageSender = sender;
325   }
326 
327   /**
328    * The {@link TaskSchedulerJobService} has started and all queued task should be executed in the
329    * worker thread.
330    */
331   @MainThread
onStartJob(Job job, List<Bundle> pendingTasks)332   public void onStartJob(Job job, List<Bundle> pendingTasks) {
333     VvmLog.i(TAG, "onStartJob");
334     mJob = job;
335     mTasks.fromBundles(this, pendingTasks);
336     maybeRunNextTask();
337   }
338 
339   /**
340    * The {@link TaskSchedulerJobService} is being terminated by the system (timeout or network
341    * lost). A new job will be queued to resume all pending tasks. The current unfinished job may be
342    * ran again.
343    */
344   @MainThread
onStopJob()345   public void onStopJob() {
346     VvmLog.e(TAG, "onStopJob");
347     if (isJobRunning()) {
348       finishJob();
349       mMainThreadHandler.post(() -> scheduleJob(0, true));
350     }
351   }
352 
353   /**
354    * Serializes all pending tasks and schedule a new {@link TaskSchedulerJobService}.
355    *
356    * @param delayMillis the delay before stating the job, see {@link
357    *     android.app.job.JobInfo.Builder#setMinimumLatency(long)}. This must be 0 if {@code
358    *     isNewJob} is true.
359    * @param isNewJob a new job will be requested to run immediately, bypassing all requirements.
360    */
361   @MainThread
scheduleJob(long delayMillis, boolean isNewJob)362   private void scheduleJob(long delayMillis, boolean isNewJob) {
363     Assert.isMainThread();
364     TaskSchedulerJobService.scheduleJob(this, serializePendingTasks(), delayMillis, isNewJob);
365     mTasks.clear();
366   }
367 
368   /**
369    * Signals {@link TaskSchedulerJobService} the current session of tasks has finished, and the wake
370    * lock can be released. Note: this only takes effect after the main thread has been returned. If
371    * a new job need to be scheduled, it should be posted on the main thread handler instead of
372    * calling directly.
373    */
374   @MainThread
finishJob()375   private void finishJob() {
376     Assert.isMainThread();
377     VvmLog.i(TAG, "finishing Job");
378     mJob.finish();
379     mJob = null;
380   }
381 
382   @Override
383   @Nullable
onBind(Intent intent)384   public IBinder onBind(Intent intent) {
385     return mBinder;
386   }
387 
388   @NeededForTesting
389   class LocalBinder extends Binder {
390 
391     @NeededForTesting
getService()392     public TaskSchedulerService getService() {
393       return TaskSchedulerService.this;
394     }
395   }
396 
isJobRunning()397   private boolean isJobRunning() {
398     return mJob != null;
399   }
400 }
401