summaryrefslogtreecommitdiffstats
path: root/java/com/android/voicemail/impl/scheduling
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-05-04 08:23:17 -0700
committerEric Erfanian <erfanian@google.com>2017-05-04 14:04:39 -0700
commit10b34a5ebf12e97ecba0caf3c8e30b476b038a96 (patch)
tree3a325b0effac02fbd228b8ddf2f96589e5df72cd /java/com/android/voicemail/impl/scheduling
parent8369df095a73a77b3715f8ae7ba06089cebca4ce (diff)
downloadandroid_packages_apps_Dialer-10b34a5ebf12e97ecba0caf3c8e30b476b038a96.tar.gz
android_packages_apps_Dialer-10b34a5ebf12e97ecba0caf3c8e30b476b038a96.tar.bz2
android_packages_apps_Dialer-10b34a5ebf12e97ecba0caf3c8e30b476b038a96.zip
Update Dialer to V10 RC16
This release was created following the instructions at: go/dialer-aosp-release Subsequent dialer releases will follow as O bugs are fixed, until we reach our final RC. Version: 10 Candidate: RC16 Branch: dialer-android_release_branch/153304843.1 dialer-android/dialer-android_20170416.00/dialer-android_20170416.00_RC16 This release contains the following bug fixes since RC00: Bug: 37324705 35304403 36067503 35304446 33203808 37280992 37346084 35766990 37481880 37424493 36470282 37347691 37519015 37168472 35805360 37545472 27704934 36515614 35766990 37577470 34739750 35801628 36788693 35264204 36708536 37628370 36904650 37314436 37642171 37530847 37637799 37666625 37548549 37648036 37636412 37323529 37630507 35919141 37198343 37548572 36178218 37640315 37663896 37720467 37275944 37710497 31634477 37744796 37348506 37744796 37568534 37672424 34872683 34873026 37681461 34873295 37748373 37526812 37618638 37663896 37536088 37727455 37165687 36651204 36900708 37323529 36902926 37256480 37328353 37432034 37436952 34093562 37720889 37321935 37780300 37781115 37755902 36588206 34258266 37290464 37698062 37618638 37473004 37432034 37918676 37870494 37722091 Test: make, on device Change-Id: I99e1a484ccd578c1f8a13e7a6a4b4952f0791297
Diffstat (limited to 'java/com/android/voicemail/impl/scheduling')
-rw-r--r--java/com/android/voicemail/impl/scheduling/BaseTask.java2
-rw-r--r--java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java2
-rw-r--r--java/com/android/voicemail/impl/scheduling/RetryPolicy.java2
-rw-r--r--java/com/android/voicemail/impl/scheduling/Task.java8
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskExecutor.java455
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskReceiver.java80
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java60
-rw-r--r--java/com/android/voicemail/impl/scheduling/Tasks.java5
8 files changed, 570 insertions, 44 deletions
diff --git a/java/com/android/voicemail/impl/scheduling/BaseTask.java b/java/com/android/voicemail/impl/scheduling/BaseTask.java
index 0144e346f..bbdca8c88 100644
--- a/java/com/android/voicemail/impl/scheduling/BaseTask.java
+++ b/java/com/android/voicemail/impl/scheduling/BaseTask.java
@@ -133,7 +133,7 @@ public abstract class BaseTask implements Task {
}
/**
- * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
+ * Creates an intent that can be used to be broadcast to the {@link TaskReceiver}. Derived class
* should build their intent upon this.
*/
public static Intent createIntent(
diff --git a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
index 76fba4fb0..342b56e8a 100644
--- a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
+++ b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
@@ -51,7 +51,7 @@ public class MinimalIntervalPolicy implements Policy {
BaseTask.createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
- mTask.getContext().startService(intent);
+ mTask.getContext().sendBroadcast(intent);
}
}
diff --git a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
index b8703ea15..c408bdc4a 100644
--- a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
+++ b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
@@ -99,7 +99,7 @@ public class RetryPolicy implements Policy {
Intent intent = mTask.createRestartIntent();
intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
- mTask.getContext().startService(intent);
+ mTask.getContext().sendBroadcast(intent);
}
@Override
diff --git a/java/com/android/voicemail/impl/scheduling/Task.java b/java/com/android/voicemail/impl/scheduling/Task.java
index 447a9db7b..484a6262e 100644
--- a/java/com/android/voicemail/impl/scheduling/Task.java
+++ b/java/com/android/voicemail/impl/scheduling/Task.java
@@ -24,8 +24,8 @@ import android.telecom.PhoneAccountHandle;
import java.util.Objects;
/**
- * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a bundle to
- * the scheduler, The task must be constructable with the bundle. Specifically, It must have a
+ * A task for {@link TaskExecutor} to execute. Since the task is sent through a bundle to the
+ * scheduler, The task must be constructable with the bundle. Specifically, It must have a
* constructor with zero arguments, and have all relevant data packed inside the bundle. Use {@link
* Tasks#createIntent(Context, Class)} to create a intent that will construct the Task.
*
@@ -112,8 +112,8 @@ public interface Task {
/**
* @return number of milliSeconds the scheduler should wait before running this task. A value less
- * than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready. If
- * no tasks are ready, the scheduler will sleep for this amount of time before doing another
+ * than {@link TaskExecutor#READY_TOLERANCE_MILLISECONDS} will be considered ready. If no
+ * tasks are ready, the scheduler will sleep for this amount of time before doing another
* check (it will still wake if a new task is added). The first task in the queue that is
* ready will be executed.
*/
diff --git a/java/com/android/voicemail/impl/scheduling/TaskExecutor.java b/java/com/android/voicemail/impl/scheduling/TaskExecutor.java
new file mode 100644
index 000000000..84dc1db4a
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskExecutor.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2017 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+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.TaskQueue.NextTask;
+import java.util.List;
+
+/**
+ * A singleton to queue and run {@link Task} with the {@link android.app.job.JobScheduler}. A task
+ * is queued by sending a broadcast to {@link TaskReceiver}. The intent should contain enough
+ * information in {@link Intent#getExtras()} to construct the task (see {@link
+ * Tasks#createIntent(Context, Class)}).
+ *
+ * <p>The executor will only exist when {@link TaskSchedulerJobService} is running.
+ *
+ * <p>All tasks are ran in the background with a wakelock being held by the {@link
+ * android.app.job.JobScheduler}, which is between {@link #onStartJob(Job, List)} and {@link
+ * #finishJobAsync()}. The {@link TaskSchedulerJobService} also has a {@link TaskQueue}, but the
+ * data is stored in the {@link android.app.job.JobScheduler} instead of the process memory, so if
+ * the process is killed the queued tasks will be restored. If a new task is added, a new {@link
+ * TaskSchedulerJobService} will be scheduled to run the task. If the job is already scheduled, the
+ * new task will be pushed into the queue of the scheduled job. If the job is already running, the
+ * job will be queued in process memory.
+ *
+ * <p>Only one task will be ran at a time, and same task cannot exist in the queue at the same time.
+ * Refer to {@link TaskQueue} for queuing and execution order.
+ *
+ * <p>If there are still tasks in the queue but none are executable immediately, the service will
+ * enter a "sleep", pushing all remaining task into a new job and end the current job.
+ *
+ * <p>The executor will be started when {@link TaskSchedulerJobService} is running, and stopped when
+ * there are no more tasks in the queue or when the executor is put to sleep.
+ *
+ * <p>{@link android.app.job.JobScheduler} is not used directly due to:
+ *
+ * <ul>
+ * <li>The {@link android.telecom.PhoneAccountHandle} used to differentiate task can not be easily
+ * mapped into an integer for job id
+ * <li>A job cannot be mutated to store information such as retry count.
+ * </ul>
+ */
+@TargetApi(VERSION_CODES.O)
+final class TaskExecutor {
+
+ /**
+ * An entity that holds execution resources for the {@link TaskExecutor} to run, usually a {@link
+ * android.app.job.JobService}.
+ */
+ interface Job {
+
+ /**
+ * Signals to Job to end and release its' resources. This is an asynchronous call and may not
+ * take effect immediately.
+ */
+ @MainThread
+ void finishAsync();
+
+ /** Whether the call to {@link #finishAsync()} has actually taken effect. */
+ @MainThread
+ boolean isFinished();
+ }
+
+ private static final String TAG = "VvmTaskExecutor";
+
+ 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.
+ *
+ * <p>A short sleep will continue the job and use {@link Handler#postDelayed(Runnable, long)} to
+ * wait for the next task.
+ *
+ * <p>A long sleep will finish the job and schedule a new one. The exact execution time is
+ * subjected to {@link android.app.job.JobScheduler} battery optimization, and is not exact.
+ */
+ private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 10_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;
+
+ /** Interval between polling of whether the job is finished. */
+ private static final int TERMINATE_POLLING_INTERVAL_MILLISECONDS = 1_000;
+
+ // The thread to run tasks on
+ private final WorkerThreadHandler workerThreadHandler;
+
+ private static TaskExecutor instance;
+
+ /**
+ * Used by tests to turn task handling into a single threaded process by calling {@link
+ * Handler#handleMessage(Message)} directly
+ */
+ private MessageSender messageSender = new MessageSender();
+
+ private final MainThreadHandler mainThreadHandler;
+
+ private final Context context;
+
+ /** Main thread only, access through {@link #getTasks()} */
+ private final TaskQueue tasks = new TaskQueue();
+
+ private boolean isWorkerThreadBusy = false;
+
+ private boolean isTerminating = false;
+
+ private Job job;
+
+ private final Runnable stopServiceWithDelay =
+ new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ VvmLog.i(TAG, "Stopping service");
+ if (!isJobRunning() || isTerminating()) {
+ VvmLog.e(TAG, "Service already stopped");
+ return;
+ }
+ scheduleJobAndTerminate(0, true);
+ }
+ };
+
+ /**
+ * Reschedule the {@link TaskSchedulerJobService} and terminate the executor when the {@link Job}
+ * is truly finished. If the job is still not finished, this runnable will requeue itself on the
+ * main thread. The requeue is only expected to happen a few times.
+ */
+ private class JobFinishedPoller implements Runnable {
+
+ private final long delayMillis;
+ private final boolean isNewJob;
+ private int invocationCounter = 0;
+
+ JobFinishedPoller(long delayMillis, boolean isNewJob) {
+ this.delayMillis = delayMillis;
+ this.isNewJob = isNewJob;
+ }
+
+ @Override
+ public void run() {
+ // The job should be finished relatively quickly. Assert to make sure this assumption is true.
+ Assert.isTrue(invocationCounter < 10);
+ invocationCounter++;
+ if (job.isFinished()) {
+ VvmLog.i("JobFinishedPoller.run", "Job finished");
+ if (!getTasks().isEmpty()) {
+ TaskSchedulerJobService.scheduleJob(
+ context, serializePendingTasks(), delayMillis, isNewJob);
+ tasks.clear();
+ }
+ terminate();
+ return;
+ }
+ VvmLog.w("JobFinishedPoller.run", "Job still running");
+ mainThreadHandler.postDelayed(this, TERMINATE_POLLING_INTERVAL_MILLISECONDS);
+ }
+ };
+
+ /** Should attempt to run the next task when a task has finished or been added. */
+ private boolean taskAutoRunDisabledForTesting = false;
+
+ @VisibleForTesting
+ final class WorkerThreadHandler extends Handler {
+
+ public WorkerThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @WorkerThread
+ public void handleMessage(Message msg) {
+ Assert.isNotMainThread();
+ Task task = (Task) msg.obj;
+ try {
+ VvmLog.i(TAG, "executing task " + task);
+ task.onExecuteInBackgroundThread();
+ } catch (Throwable throwable) {
+ VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
+ }
+
+ Message schedulerMessage = mainThreadHandler.obtainMessage();
+ schedulerMessage.obj = task;
+ messageSender.send(schedulerMessage);
+ }
+ }
+
+ @VisibleForTesting
+ final class MainThreadHandler extends Handler {
+
+ public MainThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @MainThread
+ public void handleMessage(Message msg) {
+ Assert.isMainThread();
+ Task task = (Task) msg.obj;
+ getTasks().remove(task);
+ task.onCompleted();
+ isWorkerThreadBusy = false;
+ maybeRunNextTask();
+ }
+ }
+
+ /** Starts a new TaskExecutor. May only be called by {@link TaskSchedulerJobService}. */
+ @MainThread
+ static void createRunningInstance(Context context) {
+ Assert.isMainThread();
+ Assert.isTrue(instance == null);
+ instance = new TaskExecutor(context);
+ }
+
+ /** @return the currently running instance, or {@code null} if the executor is not running. */
+ @MainThread
+ @Nullable
+ static TaskExecutor getRunningInstance() {
+ return instance;
+ }
+
+ private TaskExecutor(Context context) {
+ this.context = context;
+ HandlerThread thread = new HandlerThread("VvmTaskExecutor");
+ thread.start();
+
+ workerThreadHandler = new WorkerThreadHandler(thread.getLooper());
+ mainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
+ }
+
+ @VisibleForTesting
+ void terminate() {
+ VvmLog.i(TAG, "terminated");
+ Assert.isMainThread();
+ job = null;
+ workerThreadHandler.getLooper().quit();
+ instance = null;
+ TaskReceiver.resendDeferredBroadcasts(context);
+ }
+
+ @MainThread
+ void addTask(Task task) {
+ Assert.isMainThread();
+ getTasks().add(task);
+ VvmLog.i(TAG, task + " added");
+ mainThreadHandler.removeCallbacks(stopServiceWithDelay);
+ maybeRunNextTask();
+ }
+
+ @MainThread
+ @VisibleForTesting
+ TaskQueue getTasks() {
+ Assert.isMainThread();
+ return tasks;
+ }
+
+ @MainThread
+ private void maybeRunNextTask() {
+ Assert.isMainThread();
+ if (isWorkerThreadBusy) {
+ return;
+ }
+ if (taskAutoRunDisabledForTesting) {
+ // If taskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
+ // to run the next task.
+ return;
+ }
+
+ runNextTask();
+ }
+
+ @VisibleForTesting
+ @MainThread
+ void runNextTask() {
+ Assert.isMainThread();
+ if (getTasks().isEmpty()) {
+ prepareStop();
+ return;
+ }
+ NextTask nextTask = getTasks().getNextTask(READY_TOLERANCE_MILLISECONDS);
+
+ if (nextTask.task != null) {
+ nextTask.task.onBeforeExecute();
+ Message message = workerThreadHandler.obtainMessage();
+ message.obj = nextTask.task;
+ isWorkerThreadBusy = true;
+ messageSender.send(message);
+ return;
+ }
+ VvmLog.i(TAG, "minimal wait time:" + nextTask.minimalWaitTimeMillis);
+ if (!taskAutoRunDisabledForTesting && nextTask.minimalWaitTimeMillis != 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(nextTask.minimalWaitTimeMillis);
+ }
+ }
+
+ @MainThread
+ private void sleep(long timeMillis) {
+ VvmLog.i(TAG, "sleep for " + timeMillis + " millis");
+ if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
+ mainThreadHandler.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ maybeRunNextTask();
+ }
+ },
+ timeMillis);
+ return;
+ }
+ scheduleJobAndTerminate(timeMillis, false);
+ }
+
+ private List<Bundle> serializePendingTasks() {
+ return getTasks().toBundles();
+ }
+
+ private void prepareStop() {
+ VvmLog.i(
+ TAG,
+ "no more tasks, stopping service if no task are added in "
+ + STOP_DELAY_MILLISECONDS
+ + " millis");
+ mainThreadHandler.postDelayed(stopServiceWithDelay, STOP_DELAY_MILLISECONDS);
+ }
+
+ @NeededForTesting
+ static class MessageSender {
+
+ public void send(Message message) {
+ message.sendToTarget();
+ }
+ }
+
+ @NeededForTesting
+ void setTaskAutoRunDisabledForTest(boolean value) {
+ taskAutoRunDisabledForTesting = value;
+ }
+
+ @NeededForTesting
+ void setMessageSenderForTest(MessageSender sender) {
+ messageSender = sender;
+ }
+
+ /**
+ * The {@link TaskSchedulerJobService} has started and all queued task should be executed in the
+ * worker thread.
+ */
+ @MainThread
+ public void onStartJob(Job job, List<Bundle> pendingTasks) {
+ VvmLog.i(TAG, "onStartJob");
+ this.job = job;
+ tasks.fromBundles(context, pendingTasks);
+ maybeRunNextTask();
+ }
+
+ /**
+ * The {@link TaskSchedulerJobService} is being terminated by the system (timeout or network
+ * lost). A new job will be queued to resume all pending tasks. The current unfinished job may be
+ * ran again.
+ */
+ @MainThread
+ public void onStopJob() {
+ VvmLog.e(TAG, "onStopJob");
+ if (isJobRunning() && !isTerminating()) {
+ scheduleJobAndTerminate(0, true);
+ }
+ }
+
+ /**
+ * Send all pending tasks and schedule a new {@link TaskSchedulerJobService}. The current executor
+ * will start the termination process, but restarted when the scheduled job runs in the future.
+ *
+ * @param delayMillis the delay before stating the job, see {@link
+ * android.app.job.JobInfo.Builder#setMinimumLatency(long)}. This must be 0 if {@code
+ * isNewJob} is true.
+ * @param isNewJob a new job will be requested to run immediately, bypassing all requirements.
+ */
+ @MainThread
+ @VisibleForTesting
+ void scheduleJobAndTerminate(long delayMillis, boolean isNewJob) {
+ Assert.isMainThread();
+ finishJobAsync();
+ mainThreadHandler.post(new JobFinishedPoller(delayMillis, isNewJob));
+ }
+
+ /**
+ * Whether the TaskExecutor is still terminating. {@link TaskReceiver} should defer all new task
+ * until {@link #getRunningInstance()} returns {@code null} so a new job can be started. {@link
+ * #scheduleJobAndTerminate(long, boolean)} does not run immediately because the job can only be
+ * scheduled after the main thread has returned. The TaskExecutor will be in a intermediate state
+ * between scheduleJobAndTerminate() and terminate(). In this state, {@link #getRunningInstance()}
+ * returns non-null because it has not been fully stopped yet, but the TaskExecutor cannot do
+ * anything. A new job should not be scheduled either because the current job might still be
+ * running.
+ */
+ @MainThread
+ public boolean isTerminating() {
+ return isTerminating;
+ }
+
+ /**
+ * Signals {@link TaskSchedulerJobService} the current session of tasks has finished, and the wake
+ * lock can be released. Note: this only takes effect after the main thread has been returned. If
+ * a new job need to be scheduled, it should be posted on the main thread handler instead of
+ * calling directly.
+ */
+ @MainThread
+ private void finishJobAsync() {
+ Assert.isTrue(!isTerminating());
+ Assert.isMainThread();
+ VvmLog.i(TAG, "finishing Job");
+ job.finishAsync();
+ isTerminating = true;
+ mainThreadHandler.removeCallbacks(stopServiceWithDelay);
+ }
+
+ private boolean isJobRunning() {
+ return job != null;
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskReceiver.java b/java/com/android/voicemail/impl/scheduling/TaskReceiver.java
new file mode 100644
index 000000000..00d36d00f
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskReceiver.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import com.android.voicemail.impl.VvmLog;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * BroadcastReceiver to queue and run {@link Task} with the {@link android.app.job.JobScheduler}. A
+ * task is queued using a explicit broadcast to this receiver. The intent should contain enough
+ * information in {@link Intent#getExtras()} to construct the task (see {@link
+ * Tasks#createIntent(Context, Class)}). The task will be queued directly in {@link TaskExecutor} if
+ * it is already running, or in {@link TaskSchedulerJobService} if not.
+ */
+@TargetApi(VERSION_CODES.O)
+public class TaskReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "VvmTaskReceiver";
+
+ private static final List<Intent> deferredBroadcasts = new ArrayList<>();
+
+ /**
+ * When {@link TaskExecutor#isTerminating()} is {@code true}, newly added tasks will be deferred
+ * to allow the TaskExecutor to terminate properly. After termination is completed this should be
+ * called to add the tasks again.
+ */
+ public static void resendDeferredBroadcasts(Context context) {
+ for (Intent intent : deferredBroadcasts) {
+ context.sendBroadcast(intent);
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ VvmLog.w(TAG, "null intent received");
+ return;
+ }
+ VvmLog.i(TAG, "task received");
+ TaskExecutor taskExecutor = TaskExecutor.getRunningInstance();
+ if (taskExecutor != null) {
+ VvmLog.i(TAG, "TaskExecutor already running");
+ if (taskExecutor.isTerminating()) {
+ // The current taskExecutor and cannot do anything and a new job cannot be scheduled. Defer
+ // the task until a new job can be scheduled.
+ VvmLog.w(TAG, "TaskExecutor is terminating, bouncing task");
+ deferredBroadcasts.add(intent);
+ return;
+ }
+ Task task = Tasks.createTask(context, intent.getExtras());
+ taskExecutor.addTask(task);
+ } else {
+ VvmLog.i(TAG, "scheduling new job");
+ List<Bundle> taskList = new ArrayList<>();
+ taskList.add(intent.getExtras());
+ TaskSchedulerJobService.scheduleJob(context, taskList, 0, true);
+ }
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java
index eab410eb0..9bfce0052 100644
--- a/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java
@@ -23,11 +23,8 @@ import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
-import android.os.IBinder;
import android.os.Parcelable;
import android.support.annotation.MainThread;
import com.android.dialer.constants.ScheduledJobIds;
@@ -36,59 +33,42 @@ import com.android.voicemail.impl.VvmLog;
import java.util.ArrayList;
import java.util.List;
-/**
- * A {@link JobService} that will trigger the background execution of {@link TaskSchedulerService}.
- */
+/** A {@link JobService} that will trigger the background execution of {@link TaskExecutor}. */
@TargetApi(VERSION_CODES.O)
-public class TaskSchedulerJobService extends JobService implements TaskSchedulerService.Job {
+public class TaskSchedulerJobService extends JobService implements TaskExecutor.Job {
private static final String TAG = "TaskSchedulerJobService";
private static final String EXTRA_TASK_EXTRAS_ARRAY = "extra_task_extras_array";
private JobParameters jobParameters;
- private TaskSchedulerService scheduler;
-
- private final ServiceConnection mConnection =
- new ServiceConnection() {
-
- @Override
- public void onServiceConnected(ComponentName className, IBinder binder) {
- VvmLog.i(TAG, "TaskSchedulerService connected");
- scheduler = ((TaskSchedulerService.LocalBinder) binder).getService();
- scheduler.onStartJob(
- TaskSchedulerJobService.this,
- getBundleList(
- jobParameters.getTransientExtras().getParcelableArray(EXTRA_TASK_EXTRAS_ARRAY)));
- }
-
- @Override
- public void onServiceDisconnected(ComponentName unused) {
- // local service, process should always be killed together.
- Assert.fail();
- }
- };
@Override
@MainThread
public boolean onStartJob(JobParameters params) {
jobParameters = params;
- bindService(
- new Intent(this, TaskSchedulerService.class), mConnection, Context.BIND_AUTO_CREATE);
+ TaskExecutor.createRunningInstance(this);
+ TaskExecutor.getRunningInstance()
+ .onStartJob(
+ this,
+ getBundleList(
+ jobParameters.getTransientExtras().getParcelableArray(EXTRA_TASK_EXTRAS_ARRAY)));
return true /* job still running in background */;
}
@Override
@MainThread
public boolean onStopJob(JobParameters params) {
- scheduler.onStopJob();
+ TaskExecutor.getRunningInstance().onStopJob();
jobParameters = null;
- return false /* don't reschedule. TaskScheduler service will post a new job */;
+ return false /* don't reschedule. TaskExecutor service will post a new job */;
}
/**
* Schedule a job to run the {@code pendingTasks}. If a job is already scheduled it will be
- * appended to the back of the queue and the job will be rescheduled.
+ * appended to the back of the queue and the job will be rescheduled. A job may only be scheduled
+ * when the {@link TaskExecutor} is not running ({@link TaskExecutor#getRunningInstance()}
+ * returning {@code null})
*
* @param delayMillis delay before running the job. Must be 0 if{@code isNewJob} is true.
* @param isNewJob a new job will be forced to run immediately.
@@ -141,11 +121,19 @@ public class TaskSchedulerJobService extends JobService implements TaskScheduler
* the wakelock
*/
@Override
- public void finish() {
- VvmLog.i(TAG, "finishing job and unbinding TaskSchedulerService");
+ public void finishAsync() {
+ VvmLog.i(TAG, "finishing job");
jobFinished(jobParameters, false);
jobParameters = null;
- unbindService(mConnection);
+ }
+
+ @MainThread
+ @Override
+ public boolean isFinished() {
+ Assert.isMainThread();
+ return getSystemService(JobScheduler.class)
+ .getPendingJob(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB)
+ == null;
}
private static List<Bundle> getBundleList(Parcelable[] parcelables) {
diff --git a/java/com/android/voicemail/impl/scheduling/Tasks.java b/java/com/android/voicemail/impl/scheduling/Tasks.java
index 34debaf29..76da3d7f6 100644
--- a/java/com/android/voicemail/impl/scheduling/Tasks.java
+++ b/java/com/android/voicemail/impl/scheduling/Tasks.java
@@ -19,6 +19,7 @@ package com.android.voicemail.impl.scheduling;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import com.android.voicemail.impl.VvmLog;
/** Common operations on {@link Task} */
@@ -32,6 +33,7 @@ final class Tasks {
* Create a task from a bundle. The bundle is created either with {@link #toBundle(Task)} or
* {@link #createIntent(Context, Class)} from the target {@link Task}
*/
+ @NonNull
public static Task createTask(Context context, Bundle extras) {
// The extra contains custom parcelables which cannot be unmarshalled by the framework class
// loader.
@@ -66,7 +68,8 @@ final class Tasks {
* necessary information.
*/
public static Intent createIntent(Context context, Class<? extends Task> task) {
- Intent intent = new Intent(context, TaskSchedulerService.class);
+ Intent intent = new Intent(context, TaskReceiver.class);
+ intent.setPackage(context.getPackageName());
intent.putExtra(EXTRA_CLASS_NAME, task.getName());
return intent;
}