summaryrefslogtreecommitdiff
path: root/java/com/android/voicemail/impl/scheduling
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/voicemail/impl/scheduling')
-rw-r--r--java/com/android/voicemail/impl/scheduling/BaseTask.java36
-rw-r--r--java/com/android/voicemail/impl/scheduling/BlockerTask.java14
-rw-r--r--java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java5
-rw-r--r--java/com/android/voicemail/impl/scheduling/Policy.java4
-rw-r--r--java/com/android/voicemail/impl/scheduling/PostponePolicy.java6
-rw-r--r--java/com/android/voicemail/impl/scheduling/RetryPolicy.java11
-rw-r--r--java/com/android/voicemail/impl/scheduling/Task.java38
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskQueue.java149
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java158
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java294
-rw-r--r--java/com/android/voicemail/impl/scheduling/Tasks.java73
11 files changed, 609 insertions, 179 deletions
diff --git a/java/com/android/voicemail/impl/scheduling/BaseTask.java b/java/com/android/voicemail/impl/scheduling/BaseTask.java
index 4cc6dd59e..0144e346f 100644
--- a/java/com/android/voicemail/impl/scheduling/BaseTask.java
+++ b/java/com/android/voicemail/impl/scheduling/BaseTask.java
@@ -18,12 +18,14 @@ package com.android.voicemail.impl.scheduling;
import android.content.Context;
import android.content.Intent;
+import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.CallSuper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.telecom.PhoneAccountHandle;
+import com.android.dialer.proguard.UsedByReflection;
import com.android.voicemail.impl.Assert;
import com.android.voicemail.impl.NeededForTesting;
import java.util.ArrayList;
@@ -33,10 +35,15 @@ import java.util.List;
* Provides common utilities for task implementations, such as execution time and managing {@link
* Policy}
*/
+@UsedByReflection(value = "Tasks.java")
public abstract class BaseTask implements Task {
private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+ private static final String EXTRA_EXECUTION_TIME = "extra_execution_time";
+
+ private Bundle mExtras;
+
private Context mContext;
private int mId;
@@ -58,7 +65,7 @@ public abstract class BaseTask implements Task {
/**
* Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
- * #onCreate(Context, Intent, int, int)} returns.
+ * #onCreate(Context, Bundle)} returns.
*/
@MainThread
public void setId(int id) {
@@ -86,8 +93,7 @@ public abstract class BaseTask implements Task {
return mPhoneAccountHandle;
}
/**
- * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
- * be missed.
+ * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Bundle)} will be missed.
*/
@MainThread
public BaseTask addPolicy(Policy policy) {
@@ -107,6 +113,7 @@ public abstract class BaseTask implements Task {
mHasFailed = true;
}
+ /** @param timeMillis the time since epoch, in milliseconds. */
@MainThread
public void setExecutionTime(long timeMillis) {
Assert.isMainThread();
@@ -131,7 +138,7 @@ public abstract class BaseTask implements Task {
*/
public static Intent createIntent(
Context context, Class<? extends BaseTask> task, PhoneAccountHandle phoneAccountHandle) {
- Intent intent = TaskSchedulerService.createIntent(context, task);
+ Intent intent = Tasks.createIntent(context, task);
intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
return intent;
}
@@ -142,12 +149,27 @@ public abstract class BaseTask implements Task {
}
@Override
+ public Bundle toBundle() {
+ mExtras.putLong(EXTRA_EXECUTION_TIME, mExecutionTime);
+ return mExtras;
+ }
+
+ @Override
@CallSuper
- public void onCreate(Context context, Intent intent, int flags, int startId) {
+ public void onCreate(Context context, Bundle extras) {
mContext = context;
- mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mExtras = extras;
+ mPhoneAccountHandle = extras.getParcelable(EXTRA_PHONE_ACCOUNT_HANDLE);
for (Policy policy : mPolicies) {
- policy.onCreate(this, intent, flags, startId);
+ policy.onCreate(this, extras);
+ }
+ }
+
+ @Override
+ @CallSuper
+ public void onRestore(Bundle extras) {
+ if (mExtras.containsKey(EXTRA_EXECUTION_TIME)) {
+ mExecutionTime = extras.getLong(EXTRA_EXECUTION_TIME);
}
}
diff --git a/java/com/android/voicemail/impl/scheduling/BlockerTask.java b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
index 353508d56..1c8badaed 100644
--- a/java/com/android/voicemail/impl/scheduling/BlockerTask.java
+++ b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
@@ -17,10 +17,12 @@
package com.android.voicemail.impl.scheduling;
import android.content.Context;
-import android.content.Intent;
+import android.os.Bundle;
+import com.android.dialer.proguard.UsedByReflection;
import com.android.voicemail.impl.VvmLog;
/** Task to block another task of the same ID from being queued for a certain amount of time. */
+@UsedByReflection(value = "Tasks.java")
public class BlockerTask extends BaseTask {
private static final String TAG = "BlockerTask";
@@ -33,10 +35,10 @@ public class BlockerTask extends BaseTask {
}
@Override
- public void onCreate(Context context, Intent intent, int flags, int startId) {
- super.onCreate(context, intent, flags, startId);
- setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
- setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
+ public void onCreate(Context context, Bundle extras) {
+ super.onCreate(context, extras);
+ setId(extras.getInt(EXTRA_TASK_ID, TASK_INVALID));
+ setExecutionTime(getTimeMillis() + extras.getInt(EXTRA_BLOCK_FOR_MILLIS, 0));
}
@Override
@@ -46,6 +48,6 @@ public class BlockerTask extends BaseTask {
@Override
public void onDuplicatedTaskAdded(Task task) {
- VvmLog.v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
+ VvmLog.i(TAG, task + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
}
}
diff --git a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
index 8b2fe7098..76fba4fb0 100644
--- a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
+++ b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
@@ -17,6 +17,7 @@
package com.android.voicemail.impl.scheduling;
import android.content.Intent;
+import android.os.Bundle;
import com.android.voicemail.impl.scheduling.Task.TaskId;
/**
@@ -35,7 +36,7 @@ public class MinimalIntervalPolicy implements Policy {
}
@Override
- public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ public void onCreate(BaseTask task, Bundle extras) {
mTask = task;
mId = mTask.getId();
}
@@ -47,7 +48,7 @@ public class MinimalIntervalPolicy implements Policy {
public void onCompleted() {
if (!mTask.hasFailed()) {
Intent intent =
- mTask.createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
+ 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);
diff --git a/java/com/android/voicemail/impl/scheduling/Policy.java b/java/com/android/voicemail/impl/scheduling/Policy.java
index 607782191..9624aeb7d 100644
--- a/java/com/android/voicemail/impl/scheduling/Policy.java
+++ b/java/com/android/voicemail/impl/scheduling/Policy.java
@@ -16,7 +16,7 @@
package com.android.voicemail.impl.scheduling;
-import android.content.Intent;
+import android.os.Bundle;
/**
* A set of listeners managed by {@link BaseTask} for common behaviors such as retrying. Call {@link
@@ -24,7 +24,7 @@ import android.content.Intent;
*/
public interface Policy {
- void onCreate(BaseTask task, Intent intent, int flags, int startId);
+ void onCreate(BaseTask task, Bundle extras);
void onBeforeExecute();
diff --git a/java/com/android/voicemail/impl/scheduling/PostponePolicy.java b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
index e24df0c7a..46773b53a 100644
--- a/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
+++ b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
@@ -16,7 +16,7 @@
package com.android.voicemail.impl.scheduling;
-import android.content.Intent;
+import android.os.Bundle;
import com.android.voicemail.impl.VvmLog;
/**
@@ -37,7 +37,7 @@ public class PostponePolicy implements Policy {
}
@Override
- public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ public void onCreate(BaseTask task, Bundle extras) {
mTask = task;
mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
}
@@ -62,7 +62,7 @@ public class PostponePolicy implements Policy {
if (mTask.hasStarted()) {
return;
}
- VvmLog.d(TAG, "postponing " + mTask);
+ VvmLog.i(TAG, "postponing " + mTask);
mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
}
}
diff --git a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
index a8e4a3d3c..b8703ea15 100644
--- a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
+++ b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
@@ -17,6 +17,7 @@
package com.android.voicemail.impl.scheduling;
import android.content.Intent;
+import android.os.Bundle;
import android.telecom.PhoneAccountHandle;
import com.android.voicemail.impl.VoicemailStatus;
import com.android.voicemail.impl.VvmLog;
@@ -60,11 +61,11 @@ public class RetryPolicy implements Policy {
}
@Override
- public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ public void onCreate(BaseTask task, Bundle extras) {
mTask = task;
- mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
+ mRetryCount = extras.getInt(EXTRA_RETRY_COUNT, 0);
if (mRetryCount > 0) {
- VvmLog.d(
+ VvmLog.i(
TAG,
"retry #" + mRetryCount + " for " + mTask + " queued, executing in " + mRetryDelayMillis);
mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
@@ -85,10 +86,10 @@ public class RetryPolicy implements Policy {
public void onCompleted() {
if (!mFailed || !hasMoreRetries()) {
if (!mFailed) {
- VvmLog.d(TAG, mTask.toString() + " completed successfully");
+ VvmLog.i(TAG, mTask + " completed successfully");
}
if (!hasMoreRetries()) {
- VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
+ VvmLog.i(TAG, "Retry limit for " + mTask + " reached");
}
VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
mVoicemailStatusEditor.deferredApply();
diff --git a/java/com/android/voicemail/impl/scheduling/Task.java b/java/com/android/voicemail/impl/scheduling/Task.java
index 2d08f5b03..447a9db7b 100644
--- a/java/com/android/voicemail/impl/scheduling/Task.java
+++ b/java/com/android/voicemail/impl/scheduling/Task.java
@@ -17,26 +17,24 @@
package com.android.voicemail.impl.scheduling;
import android.content.Context;
-import android.content.Intent;
+import android.os.Bundle;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
import android.telecom.PhoneAccountHandle;
import java.util.Objects;
/**
- * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
- * the scheduler, The task must be constructable with the intent. Specifically, It must have a
- * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
- * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
- * Task.
+ * 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
+ * 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.
*
* <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
*/
public interface Task {
-
/**
* TaskId to indicate it has not be set. If a task does not provide a default TaskId it should be
- * set before {@link Task#onCreate(Context, Intent, int, int) returns}
+ * set before {@link Task#onCreate(Context, Bundle)} returns
*/
int TASK_INVALID = -1;
@@ -49,6 +47,7 @@ public interface Task {
int TASK_UPLOAD = 1;
int TASK_SYNC = 2;
int TASK_ACTIVATION = 3;
+ int TASK_STATUS_CHECK = 4;
/**
* Used to differentiate between types of tasks. If a task with the same TaskId is already in the
@@ -87,8 +86,29 @@ public interface Task {
TaskId getId();
+ /**
+ * Serializes the task into a bundle, which will be stored in a {@link android.app.job.JobInfo}
+ * and used to reconstruct the task even if the app is terminated. The task will be initialized
+ * with {@link #onCreate(Context, Bundle)}.
+ */
+ Bundle toBundle();
+
+ /**
+ * A task object is created through reflection, calling the default constructor. The actual
+ * initialization is done in this method. If the task is not a new instance, but being restored
+ * from a bundle, {@link #onRestore(Bundle)} will be called afterwards.
+ */
+ @MainThread
+ void onCreate(Context context, Bundle extras);
+
+ /**
+ * Called after {@link #onCreate(Context, Bundle)} if the task is being restored from a Bundle
+ * instead creating a new instance. For example, if the task is stored in {@link
+ * TaskSchedulerJobService} during a long sleep, this will be called when the job is ran again and
+ * the tasks are being restored from the saved state.
+ */
@MainThread
- void onCreate(Context context, Intent intent, int flags, int startId);
+ void onRestore(Bundle extras);
/**
* @return number of milliSeconds the scheduler should wait before running this task. A value less
diff --git a/java/com/android/voicemail/impl/scheduling/TaskQueue.java b/java/com/android/voicemail/impl/scheduling/TaskQueue.java
new file mode 100644
index 000000000..fc5aa947a
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskQueue.java
@@ -0,0 +1,149 @@
+/*
+ * 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.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * A queue that manages priority and duplication of {@link Task}. A task is identified by a {@link
+ * TaskId}, which consists of an integer representing the operation the task, and a {@link
+ * android.telecom.PhoneAccountHandle} representing which SIM it is operated on.
+ */
+class TaskQueue implements Iterable<Task> {
+
+ private final Queue<Task> queue = new ArrayDeque<>();
+
+ public List<Bundle> toBundles() {
+ List<Bundle> result = new ArrayList<>(queue.size());
+ for (Task task : queue) {
+ result.add(Tasks.toBundle(task));
+ }
+ return result;
+ }
+
+ public void fromBundles(Context context, List<Bundle> pendingTasks) {
+ Assert.isTrue(queue.isEmpty());
+ for (Bundle pendingTask : pendingTasks) {
+ Task task = Tasks.createTask(context, pendingTask);
+ task.onRestore(pendingTask);
+ add(task);
+ }
+ }
+
+ /**
+ * Add a new task to the queue. A new task with a TaskId collision will be discarded, and {@link
+ * Task#onDuplicatedTaskAdded(Task)} will be called on the existing task.
+ *
+ * @return {@code true} if the task is added, or {@code false} if the task is discarded due to
+ * collision.
+ */
+ public boolean add(Task task) {
+ if (task.getId().id == Task.TASK_INVALID) {
+ throw new AssertionError("Task id was not set to a valid value before adding.");
+ }
+ if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
+ Task oldTask = getTask(task.getId());
+ if (oldTask != null) {
+ oldTask.onDuplicatedTaskAdded(task);
+ VvmLog.i("TaskQueue.add", "duplicated task added");
+ return false;
+ }
+ }
+ queue.add(task);
+ return true;
+ }
+
+ public void remove(Task task) {
+ queue.remove(task);
+ }
+
+ public Task getTask(TaskId id) {
+ Assert.isMainThread();
+ for (Task task : queue) {
+ if (task.getId().equals(id)) {
+ return task;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Packed return value of {@link #getNextTask(long)}. If a runnable task is found {@link
+ * #minimalWaitTimeMillis} will be {@code null}. If no tasks is runnable {@link #task} will be
+ * {@code null}, and {@link #minimalWaitTimeMillis} will contain the time to wait. If there are no
+ * tasks at all both will be {@code null}.
+ */
+ static final class NextTask {
+ @Nullable final Task task;
+ @Nullable final Long minimalWaitTimeMillis;
+
+ NextTask(@Nullable Task task, @Nullable Long minimalWaitTimeMillis) {
+ this.task = task;
+ this.minimalWaitTimeMillis = minimalWaitTimeMillis;
+ }
+ }
+
+ /**
+ * The next task is the first task with {@link Task#getReadyInMilliSeconds()} return a value less
+ * then {@code readyToleranceMillis}, in insertion order. If no task matches this criteria, the
+ * minimal value of {@link Task#getReadyInMilliSeconds()} is returned instead. If there are no
+ * tasks at all, the minimalWaitTimeMillis will also be null.
+ */
+ @NonNull
+ NextTask getNextTask(long readyToleranceMillis) {
+ Long minimalWaitTime = null;
+ for (Task task : queue) {
+ long waitTime = task.getReadyInMilliSeconds();
+ if (waitTime < readyToleranceMillis) {
+ return new NextTask(task, 0L);
+ } else {
+ if (minimalWaitTime == null || waitTime < minimalWaitTime) {
+ minimalWaitTime = waitTime;
+ }
+ }
+ }
+ return new NextTask(null, minimalWaitTime);
+ }
+
+ public void clear() {
+ queue.clear();
+ }
+
+ public int size() {
+ return queue.size();
+ }
+
+ public boolean isEmpty() {
+ return queue.isEmpty();
+ }
+
+ @Override
+ public Iterator<Task> iterator() {
+ return queue.iterator();
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java
new file mode 100644
index 000000000..eab410eb0
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerJobService.java
@@ -0,0 +1,158 @@
+/*
+ * 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.app.job.JobInfo;
+import android.app.job.JobParameters;
+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;
+import com.android.voicemail.impl.Assert;
+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}.
+ */
+@TargetApi(VERSION_CODES.O)
+public class TaskSchedulerJobService extends JobService implements TaskSchedulerService.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);
+ return true /* job still running in background */;
+ }
+
+ @Override
+ @MainThread
+ public boolean onStopJob(JobParameters params) {
+ scheduler.onStopJob();
+ jobParameters = null;
+ return false /* don't reschedule. TaskScheduler 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.
+ *
+ * @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.
+ */
+ @MainThread
+ public static void scheduleJob(
+ Context context, List<Bundle> pendingTasks, long delayMillis, boolean isNewJob) {
+ Assert.isMainThread();
+ JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ JobInfo pendingJob = jobScheduler.getPendingJob(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB);
+ VvmLog.i(TAG, "scheduling job with " + pendingTasks.size() + " tasks");
+ if (pendingJob != null) {
+ if (isNewJob) {
+ List<Bundle> existingTasks =
+ getBundleList(
+ pendingJob.getTransientExtras().getParcelableArray(EXTRA_TASK_EXTRAS_ARRAY));
+ VvmLog.i(TAG, "merging job with " + existingTasks.size() + " existing tasks");
+ TaskQueue queue = new TaskQueue();
+ queue.fromBundles(context, existingTasks);
+ for (Bundle pendingTask : pendingTasks) {
+ queue.add(Tasks.createTask(context, pendingTask));
+ }
+ pendingTasks = queue.toBundles();
+ }
+ VvmLog.i(TAG, "canceling existing job.");
+ jobScheduler.cancel(ScheduledJobIds.VVM_TASK_SCHEDULER_JOB);
+ }
+ Bundle extras = new Bundle();
+ extras.putParcelableArray(
+ EXTRA_TASK_EXTRAS_ARRAY, pendingTasks.toArray(new Bundle[pendingTasks.size()]));
+ JobInfo.Builder builder =
+ new JobInfo.Builder(
+ ScheduledJobIds.VVM_TASK_SCHEDULER_JOB,
+ new ComponentName(context, TaskSchedulerJobService.class))
+ .setTransientExtras(extras)
+ .setMinimumLatency(delayMillis)
+ .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+ if (isNewJob) {
+ Assert.isTrue(delayMillis == 0);
+ builder.setOverrideDeadline(0);
+ VvmLog.i(TAG, "running job instantly.");
+ }
+ jobScheduler.schedule(builder.build());
+ VvmLog.i(TAG, "job scheduled");
+ }
+
+ /**
+ * The system will hold a wakelock when {@link #onStartJob(JobParameters)} is called to ensure the
+ * device will not sleep when the job is still running. Finish the job so the system will release
+ * the wakelock
+ */
+ @Override
+ public void finish() {
+ VvmLog.i(TAG, "finishing job and unbinding TaskSchedulerService");
+ jobFinished(jobParameters, false);
+ jobParameters = null;
+ unbindService(mConnection);
+ }
+
+ private static List<Bundle> getBundleList(Parcelable[] parcelables) {
+ List<Bundle> result = new ArrayList<>(parcelables.length);
+ for (Parcelable parcelable : parcelables) {
+ result.add((Bundle) parcelable);
+ }
+ return result;
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
index 81bd36fee..5ad2447de 100644
--- a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
@@ -16,20 +16,18 @@
package com.android.voicemail.impl.scheduling;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
+import android.annotation.TargetApi;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
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;
@@ -37,20 +35,50 @@ 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;
+import com.android.voicemail.impl.scheduling.TaskQueue.NextTask;
+import java.util.List;
/**
- * 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.
+ * A service to queue and run {@link Task} with the {@link android.app.job.JobScheduler}. A task is
+ * queued using {@link Context#startService(Intent)}. The intent should contain enough information
+ * in {@link Intent#getExtras()} to construct the task (see {@link Tasks#createIntent(Context,
+ * Class)}).
+ *
+ * <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
+ * #finishJob()}. 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 service will be started when a intent is received, and stopped when there are no more
+ * tasks in the queue.
+ *
+ * <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>
*/
+@SuppressWarnings("AndroidApiChecker") /* stream() */
+@TargetApi(VERSION_CODES.O)
public class TaskSchedulerService extends Service {
- private static final String TAG = "VvmTaskScheduler";
+ interface Job {
+ void finish();
+ }
- private static final String ACTION_WAKEUP = "action_wakeup";
+ private static final String TAG = "VvmTaskScheduler";
private static final int READY_TOLERANCE_MILLISECONDS = 100;
@@ -58,15 +86,13 @@ public class TaskSchedulerService extends Service {
* 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 to held the wake lock and use {@link
- * Handler#postDelayed(Runnable, long)} to wait for the next task.
+ * <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 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.
+ * <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 = 60_000;
+ 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,
@@ -75,14 +101,9 @@ public class TaskSchedulerService extends Service {
*/
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
@@ -91,21 +112,27 @@ public class TaskSchedulerService extends Service {
private MainThreadHandler mMainThreadHandler;
- private WakeLock mWakeLock;
+ // Binder given to clients
+ private final IBinder mBinder = new LocalBinder();
/** Main thread only, access through {@link #getTasks()} */
- private final Queue<Task> mTasks = new ArrayDeque<>();
+ private final TaskQueue mTasks = new TaskQueue();
private boolean mWorkerThreadIsBusy = false;
+ private Job mJob;
+
private final Runnable mStopServiceWithDelay =
new Runnable() {
+ @MainThread
@Override
public void run() {
- VvmLog.d(TAG, "Stopping service");
+ VvmLog.i(TAG, "Stopping service");
+ finishJob();
stopSelf();
}
};
+
/** Should attempt to run the next task when a task has finished or been added. */
private boolean mTaskAutoRunDisabledForTesting = false;
@@ -122,7 +149,7 @@ public class TaskSchedulerService extends Service {
Assert.isNotMainThread();
Task task = (Task) msg.obj;
try {
- VvmLog.v(TAG, "executing task " + task);
+ VvmLog.i(TAG, "executing task " + task);
task.onExecuteInBackgroundThread();
} catch (Throwable throwable) {
VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
@@ -157,10 +184,6 @@ public class TaskSchedulerService extends Service {
@MainThread
public void onCreate() {
super.onCreate();
- mWakeLock =
- getSystemService(PowerManager.class)
- .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
- mWakeLock.setReferenceCounted(false);
HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
thread.start();
@@ -171,27 +194,27 @@ public class TaskSchedulerService extends Service {
@Override
public void onDestroy() {
mWorkerThreadHandler.getLooper().quit();
- mWakeLock.release();
}
@Override
@MainThread
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
Assert.isMainThread();
- // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
- // the service.
- mWakeLock.acquire();
- if (ACTION_WAKEUP.equals(intent.getAction())) {
- VvmLog.d(TAG, "woke up by AlarmManager");
+ if (intent == null) {
+ VvmLog.w(TAG, "null intent received");
+ return START_NOT_STICKY;
+ }
+ Task task = Tasks.createTask(this, intent.getExtras());
+ Assert.isTrue(task != null);
+ addTask(task);
+
+ mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
+ VvmLog.i(TAG, "task added");
+ if (mJob == null) {
+ scheduleJob(0, true);
} else {
- Task task = createTask(intent, flags, startId);
- if (task == null) {
- VvmLog.e(TAG, "cannot create task form intent");
- } else {
- addTask(task);
- }
+ maybeRunNextTask();
}
- maybeRunNextTask();
// STICKY means the service will be automatically restarted will the last intent if it is
// killed.
return START_NOT_STICKY;
@@ -201,66 +224,14 @@ public class TaskSchedulerService extends Service {
@VisibleForTesting
void addTask(Task task) {
Assert.isMainThread();
- if (task.getId().id == Task.TASK_INVALID) {
- throw new AssertionError("Task id was not set to a valid value before adding.");
- }
- if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
- Task oldTask = getTask(task.getId());
- if (oldTask != null) {
- oldTask.onDuplicatedTaskAdded(task);
- return;
- }
- }
- mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
getTasks().add(task);
- maybeRunNextTask();
- }
-
- @MainThread
- @Nullable
- private Task getTask(TaskId taskId) {
- Assert.isMainThread();
- for (Task task : getTasks()) {
- if (task.getId().equals(taskId)) {
- return task;
- }
- }
- return null;
}
@MainThread
- private Queue<Task> getTasks() {
- Assert.isMainThread();
- return mTasks;
- }
-
- /** Create an intent that will queue the <code>task</code> */
- 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) {
+ TaskQueue getTasks() {
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);
- }
+ return mTasks;
}
@MainThread
@@ -282,37 +253,31 @@ public class TaskSchedulerService extends Service {
@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;
- }
- }
+ NextTask nextTask = getTasks().getNextTask(READY_TOLERANCE_MILLISECONDS);
+
+ if (nextTask.task != null) {
+ nextTask.task.onBeforeExecute();
+ Message message = mWorkerThreadHandler.obtainMessage();
+ message.obj = nextTask.task;
+ mWorkerThreadIsBusy = true;
+ mMessageSender.send(message);
+ return;
}
- VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
- if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
+ VvmLog.i(TAG, "minimal wait time:" + nextTask.minimalWaitTimeMillis);
+ if (!mTaskAutoRunDisabledForTesting && 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(minimalWaitTime);
+ sleep(nextTask.minimalWaitTimeMillis);
}
}
+ @MainThread
private void sleep(long timeMillis) {
+ VvmLog.i(TAG, "sleep for " + timeMillis + " millis");
if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
mMainThreadHandler.postDelayed(
new Runnable() {
@@ -324,34 +289,24 @@ public class TaskSchedulerService extends Service {
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");
+ finishJob();
+ mMainThreadHandler.post(() -> scheduleJob(timeMillis, false));
}
- private PendingIntent getWakeupIntent() {
- Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
- return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ private List<Bundle> serializePendingTasks() {
+ return getTasks().toBundles();
}
private void prepareStop() {
- VvmLog.d(
+ VvmLog.i(
TAG,
- "No more tasks, stopping service if no task are added in "
+ "no more tasks, stopping service if no task are added in "
+ STOP_DELAY_MILLISECONDS
+ " millis");
mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
}
+ @NeededForTesting
static class MessageSender {
public void send(Message message) {
@@ -360,11 +315,6 @@ public class TaskSchedulerService extends Service {
}
@NeededForTesting
- void setContextForTest(Context context) {
- mContext = context;
- }
-
- @NeededForTesting
void setTaskAutoRunDisabledForTest(boolean value) {
mTaskAutoRunDisabledForTesting = value;
}
@@ -374,15 +324,65 @@ public class TaskSchedulerService extends Service {
mMessageSender = sender;
}
- @NeededForTesting
- void clearTasksForTest() {
+ /**
+ * 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");
+ mJob = job;
+ mTasks.fromBundles(this, 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()) {
+ finishJob();
+ mMainThreadHandler.post(() -> scheduleJob(0, true));
+ }
+ }
+
+ /**
+ * Serializes all pending tasks and schedule a new {@link TaskSchedulerJobService}.
+ *
+ * @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
+ private void scheduleJob(long delayMillis, boolean isNewJob) {
+ Assert.isMainThread();
+ TaskSchedulerJobService.scheduleJob(this, serializePendingTasks(), delayMillis, isNewJob);
mTasks.clear();
}
+ /**
+ * 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 finishJob() {
+ Assert.isMainThread();
+ VvmLog.i(TAG, "finishing Job");
+ mJob.finish();
+ mJob = null;
+ }
+
@Override
@Nullable
public IBinder onBind(Intent intent) {
- return new LocalBinder();
+ return mBinder;
}
@NeededForTesting
@@ -393,4 +393,8 @@ public class TaskSchedulerService extends Service {
return TaskSchedulerService.this;
}
}
+
+ private boolean isJobRunning() {
+ return mJob != null;
+ }
}
diff --git a/java/com/android/voicemail/impl/scheduling/Tasks.java b/java/com/android/voicemail/impl/scheduling/Tasks.java
new file mode 100644
index 000000000..34debaf29
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Tasks.java
@@ -0,0 +1,73 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import com.android.voicemail.impl.VvmLog;
+
+/** Common operations on {@link Task} */
+final class Tasks {
+
+ private Tasks() {}
+
+ static final String EXTRA_CLASS_NAME = "extra_class_name";
+
+ /**
+ * 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}
+ */
+ public static Task createTask(Context context, Bundle extras) {
+ // The extra contains custom parcelables which cannot be unmarshalled by the framework class
+ // loader.
+ extras.setClassLoader(context.getClassLoader());
+ String className = extras.getString(EXTRA_CLASS_NAME);
+ VvmLog.i("Task.createTask", "create task:" + className);
+ if (className == null) {
+ throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
+ }
+ try {
+ Task task = (Task) Class.forName(className).getDeclaredConstructor().newInstance();
+ task.onCreate(context, extras);
+ return task;
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Serializes necessary states to a bundle that can be used to restore the task with {@link
+ * #createTask(Context, Bundle)}
+ */
+ public static Bundle toBundle(Task task) {
+ Bundle result = task.toBundle();
+ result.putString(EXTRA_CLASS_NAME, task.getClass().getName());
+ return result;
+ }
+
+ /**
+ * Create an intent that when called with {@link Context#startService(Intent)}, will queue the
+ * <code>task</code>. Implementations of {@link Task} should use the result of this and fill in
+ * necessary information.
+ */
+ 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;
+ }
+}