/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.voicemail.impl.scheduling; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import com.android.voicemail.impl.Assert; import com.android.voicemail.impl.NeededForTesting; import com.android.voicemail.impl.VvmLog; import com.android.voicemail.impl.scheduling.Task.TaskId; import java.util.ArrayDeque; import java.util.Queue; /** * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time, * and same task cannot exist in the queue at the same time. The service will be started when a * intent is received, and stopped when there are no more tasks in the queue. */ public class TaskSchedulerService extends Service { private static final String TAG = "VvmTaskScheduler"; private static final String ACTION_WAKEUP = "action_wakeup"; private static final int READY_TOLERANCE_MILLISECONDS = 100; /** * Threshold to determine whether to do a short or long sleep when a task is scheduled in the * future. * *
A short sleep will continue to held the wake lock and use {@link * Handler#postDelayed(Runnable, long)} to wait for the next task. * *
A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
* exact and will wake up the device. Note: as this service is run in the telephony process it
* does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
* unbundled version should take doze into account.
*/
private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
/**
* When there are no more tasks to be run the service should be stopped. But when all tasks has
* finished there might still be more tasks in the message queue waiting to be processed,
* especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
* the service to make sure there are no pending messages.
*/
private static final int STOP_DELAY_MILLISECONDS = 5_000;
private static final String EXTRA_CLASS_NAME = "extra_class_name";
private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
// The thread to run tasks on
private volatile WorkerThreadHandler mWorkerThreadHandler;
private Context mContext = this;
/**
* Used by tests to turn task handling into a single threaded process by calling {@link
* Handler#handleMessage(Message)} directly
*/
private MessageSender mMessageSender = new MessageSender();
private MainThreadHandler mMainThreadHandler;
private WakeLock mWakeLock;
/** Main thread only, access through {@link #getTasks()} */
private final Queuetask
*/
public static Intent createIntent(Context context, Class extends Task> task) {
Intent intent = new Intent(context, TaskSchedulerService.class);
intent.putExtra(EXTRA_CLASS_NAME, task.getName());
return intent;
}
@VisibleForTesting
@MainThread
@Nullable
Task createTask(@Nullable Intent intent, int flags, int startId) {
Assert.isMainThread();
if (intent == null) {
return null;
}
String className = intent.getStringExtra(EXTRA_CLASS_NAME);
VvmLog.d(TAG, "create task:" + className);
if (className == null) {
throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
}
try {
Task task = (Task) Class.forName(className).newInstance();
task.onCreate(mContext, intent, flags, startId);
return task;
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throw new IllegalArgumentException(e);
}
}
@MainThread
private void maybeRunNextTask() {
Assert.isMainThread();
if (mWorkerThreadIsBusy) {
return;
}
if (mTaskAutoRunDisabledForTesting) {
// If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
// to run the next task.
return;
}
runNextTask();
}
@VisibleForTesting
@MainThread
void runNextTask() {
Assert.isMainThread();
// The current alarm is no longer valid, a new one will be set up if required.
getSystemService(AlarmManager.class).cancel(getWakeupIntent());
if (getTasks().isEmpty()) {
prepareStop();
return;
}
Long minimalWaitTime = null;
for (Task task : getTasks()) {
long waitTime = task.getReadyInMilliSeconds();
if (waitTime < READY_TOLERANCE_MILLISECONDS) {
task.onBeforeExecute();
Message message = mWorkerThreadHandler.obtainMessage();
message.obj = task;
mWorkerThreadIsBusy = true;
mMessageSender.send(message);
return;
} else {
if (minimalWaitTime == null || waitTime < minimalWaitTime) {
minimalWaitTime = waitTime;
}
}
}
VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
// No tasks are currently ready. Sleep until the next one should be.
// If a new task is added during the sleep the service will wake immediately.
sleep(minimalWaitTime);
}
}
private void sleep(long timeMillis) {
if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
mMainThreadHandler.postDelayed(
new Runnable() {
@Override
public void run() {
maybeRunNextTask();
}
},
timeMillis);
return;
}
// Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
// optimize the battery usage. As this service currently run in the telephony process the
// OS give it privileges to behave the same as setExact(), but set() is the targeted
// behavior once this is unbundled.
getSystemService(AlarmManager.class)
.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + timeMillis,
getWakeupIntent());
mWakeLock.release();
VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
}
private PendingIntent getWakeupIntent() {
Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
private void prepareStop() {
VvmLog.d(
TAG,
"No more tasks, stopping service if no task are added in "
+ STOP_DELAY_MILLISECONDS
+ " millis");
mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
}
static class MessageSender {
public void send(Message message) {
message.sendToTarget();
}
}
@NeededForTesting
void setContextForTest(Context context) {
mContext = context;
}
@NeededForTesting
void setTaskAutoRunDisabledForTest(boolean value) {
mTaskAutoRunDisabledForTesting = value;
}
@NeededForTesting
void setMessageSenderForTest(MessageSender sender) {
mMessageSender = sender;
}
@NeededForTesting
void clearTasksForTest() {
mTasks.clear();
}
@Override
@Nullable
public IBinder onBind(Intent intent) {
return new LocalBinder();
}
@NeededForTesting
class LocalBinder extends Binder {
@NeededForTesting
public TaskSchedulerService getService() {
return TaskSchedulerService.this;
}
}
}