diff options
Diffstat (limited to 'java/com/android/voicemail/impl/scheduling')
8 files changed, 612 insertions, 45 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..e3b718e50 --- /dev/null +++ b/java/com/android/voicemail/impl/scheduling/TaskExecutor.java @@ -0,0 +1,462 @@ +/* + * 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; + + /** Handles execution of the background task in teh worker thread. */ + @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); + } + } + + /** Handles completion of the background task in the main thread. */ + @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; + if (!isJobRunning() || isTerminating()) { + // TaskExecutor was terminated when the task is running in background, don't need to run the + // next task or terminate again + return; + } + 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..e78dcf72c --- /dev/null +++ b/java/com/android/voicemail/impl/scheduling/TaskReceiver.java @@ -0,0 +1,81 @@ +/* + * 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); + } + deferredBroadcasts.clear(); + } + + @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.getApplicationContext(), intent.getExtras()); + taskExecutor.addTask(task); + } else { + VvmLog.i(TAG, "scheduling new job"); + List<Bundle> taskList = new ArrayList<>(); + taskList.add(intent.getExtras()); + TaskSchedulerJobService.scheduleJob(context.getApplicationContext(), 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..107234edc 100644 --- a/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java +++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java @@ -23,12 +23,11 @@ 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.content.SharedPreferences; import android.os.Build.VERSION_CODES; import android.os.Bundle; -import android.os.IBinder; import android.os.Parcelable; +import android.preference.PreferenceManager; import android.support.annotation.MainThread; import com.android.dialer.constants.ScheduledJobIds; import com.android.voicemail.impl.Assert; @@ -36,59 +35,59 @@ 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))); - } + private static final String EXTRA_JOB_ID = "extra_job_id"; - @Override - public void onServiceDisconnected(ComponentName unused) { - // local service, process should always be killed together. - Assert.fail(); - } - }; + private static final String EXPECTED_JOB_ID = + "com.android.voicemail.impl.scheduling.TaskSchedulerJobService.EXPECTED_JOB_ID"; + + private static final String NEXT_JOB_ID = + "com.android.voicemail.impl.scheduling.TaskSchedulerJobService.NEXT_JOB_ID"; + + private JobParameters jobParameters; @Override @MainThread public boolean onStartJob(JobParameters params) { + int jobId = params.getTransientExtras().getInt(EXTRA_JOB_ID); + int expectedJobId = + PreferenceManager.getDefaultSharedPreferences(this).getInt(EXPECTED_JOB_ID, 0); + if (jobId != expectedJobId) { + VvmLog.e( + TAG, "Job " + jobId + " is not the last scheduled job " + expectedJobId + ", ignoring"); + return false; // nothing more to do. Job not running in background. + } + VvmLog.i(TAG, "starting " + jobId); 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. @@ -117,6 +116,13 @@ public class TaskSchedulerJobService extends JobService implements TaskScheduler jobScheduler.cancel(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB); } Bundle extras = new Bundle(); + int jobId = createJobId(context); + extras.putInt(EXTRA_JOB_ID, jobId); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putInt(EXPECTED_JOB_ID, jobId) + .apply(); + extras.putParcelableArray( EXTRA_TASK_EXTRAS_ARRAY, pendingTasks.toArray(new Bundle[pendingTasks.size()])); JobInfo.Builder builder = @@ -132,7 +138,7 @@ public class TaskSchedulerJobService extends JobService implements TaskScheduler VvmLog.i(TAG, "running job instantly."); } jobScheduler.schedule(builder.build()); - VvmLog.i(TAG, "job scheduled"); + VvmLog.i(TAG, "job " + jobId + " scheduled"); } /** @@ -141,11 +147,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) { @@ -155,4 +169,11 @@ public class TaskSchedulerJobService extends JobService implements TaskScheduler } return result; } + + private static int createJobId(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + int jobId = sharedPreferences.getInt(NEXT_JOB_ID, 0); + sharedPreferences.edit().putInt(NEXT_JOB_ID, jobId + 1).apply(); + return jobId; + } } 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; } |