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