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.java202
-rw-r--r--java/com/android/voicemail/impl/scheduling/BlockerTask.java51
-rw-r--r--java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java62
-rw-r--r--java/com/android/voicemail/impl/scheduling/Policy.java36
-rw-r--r--java/com/android/voicemail/impl/scheduling/PostponePolicy.java68
-rw-r--r--java/com/android/voicemail/impl/scheduling/RetryPolicy.java111
-rw-r--r--java/com/android/voicemail/impl/scheduling/Task.java128
-rw-r--r--java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java396
8 files changed, 1054 insertions, 0 deletions
diff --git a/java/com/android/voicemail/impl/scheduling/BaseTask.java b/java/com/android/voicemail/impl/scheduling/BaseTask.java
new file mode 100644
index 000000000..4cc6dd59e
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BaseTask.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+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.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides common utilities for task implementations, such as execution time and managing {@link
+ * Policy}
+ */
+public abstract class BaseTask implements Task {
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+ private Context mContext;
+
+ private int mId;
+ private PhoneAccountHandle mPhoneAccountHandle;
+
+ private boolean mHasStarted;
+ private volatile boolean mHasFailed;
+
+ @NonNull private final List<Policy> mPolicies = new ArrayList<>();
+
+ private long mExecutionTime;
+
+ private static Clock sClock = new Clock();
+
+ protected BaseTask(int id) {
+ mId = id;
+ mExecutionTime = getTimeMillis();
+ }
+
+ /**
+ * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
+ * #onCreate(Context, Intent, int, int)} returns.
+ */
+ @MainThread
+ public void setId(int id) {
+ Assert.isMainThread();
+ mId = id;
+ }
+
+ @MainThread
+ public boolean hasStarted() {
+ Assert.isMainThread();
+ return mHasStarted;
+ }
+
+ @MainThread
+ public boolean hasFailed() {
+ Assert.isMainThread();
+ return mHasFailed;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+ /**
+ * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
+ * be missed.
+ */
+ @MainThread
+ public BaseTask addPolicy(Policy policy) {
+ Assert.isMainThread();
+ mPolicies.add(policy);
+ return this;
+ }
+
+ /**
+ * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
+ * ends. This mechanism is used by policies for actions such as determining whether to schedule a
+ * retry. Must be call inside {@link #onExecuteInBackgroundThread()}
+ */
+ @WorkerThread
+ public void fail() {
+ Assert.isNotMainThread();
+ mHasFailed = true;
+ }
+
+ @MainThread
+ public void setExecutionTime(long timeMillis) {
+ Assert.isMainThread();
+ mExecutionTime = timeMillis;
+ }
+
+ public long getTimeMillis() {
+ return sClock.getTimeMillis();
+ }
+
+ /**
+ * Creates an intent that can be used to restart the current task. Derived class should build
+ * their intent upon this.
+ */
+ public Intent createRestartIntent() {
+ return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
+ }
+
+ /**
+ * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
+ * should build their intent upon this.
+ */
+ public static Intent createIntent(
+ Context context, Class<? extends BaseTask> task, PhoneAccountHandle phoneAccountHandle) {
+ Intent intent = TaskSchedulerService.createIntent(context, task);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ return intent;
+ }
+
+ @Override
+ public TaskId getId() {
+ return new TaskId(mId, mPhoneAccountHandle);
+ }
+
+ @Override
+ @CallSuper
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ mContext = context;
+ mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ for (Policy policy : mPolicies) {
+ policy.onCreate(this, intent, flags, startId);
+ }
+ }
+
+ @Override
+ public long getReadyInMilliSeconds() {
+ return mExecutionTime - getTimeMillis();
+ }
+
+ @Override
+ @CallSuper
+ public void onBeforeExecute() {
+ for (Policy policy : mPolicies) {
+ policy.onBeforeExecute();
+ }
+ mHasStarted = true;
+ }
+
+ @Override
+ @CallSuper
+ public void onCompleted() {
+ if (mHasFailed) {
+ for (Policy policy : mPolicies) {
+ policy.onFail();
+ }
+ }
+
+ for (Policy policy : mPolicies) {
+ policy.onCompleted();
+ }
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded(Task task) {
+ for (Policy policy : mPolicies) {
+ policy.onDuplicatedTaskAdded();
+ }
+ }
+
+ @NeededForTesting
+ static class Clock {
+
+ public long getTimeMillis() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+
+ /** Used to replace the clock with an deterministic clock */
+ @NeededForTesting
+ static void setClockForTesting(Clock clock) {
+ sClock = clock;
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/BlockerTask.java b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
new file mode 100644
index 000000000..353508d56
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/** Task to block another task of the same ID from being queued for a certain amount of time. */
+public class BlockerTask extends BaseTask {
+
+ private static final String TAG = "BlockerTask";
+
+ public static final String EXTRA_TASK_ID = "extra_task_id";
+ public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
+
+ public BlockerTask() {
+ super(TASK_INVALID);
+ }
+
+ @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));
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded(Task task) {
+ VvmLog.v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
+ }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
new file mode 100644
index 000000000..8b2fe7098
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+
+/**
+ * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
+ * task will be queued immediately, preventing the same task from running for a certain amount of
+ * time.
+ */
+public class MinimalIntervalPolicy implements Policy {
+
+ BaseTask mTask;
+ TaskId mId;
+ int mBlockForMillis;
+
+ public MinimalIntervalPolicy(int blockForMillis) {
+ mBlockForMillis = blockForMillis;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mId = mTask.getId();
+ }
+
+ @Override
+ public void onBeforeExecute() {}
+
+ @Override
+ public void onCompleted() {
+ if (!mTask.hasFailed()) {
+ Intent intent =
+ mTask.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);
+ }
+ }
+
+ @Override
+ public void onFail() {}
+
+ @Override
+ public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemail/impl/scheduling/Policy.java b/java/com/android/voicemail/impl/scheduling/Policy.java
new file mode 100644
index 000000000..607782191
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Policy.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+
+/**
+ * A set of listeners managed by {@link BaseTask} for common behaviors such as retrying. Call {@link
+ * BaseTask#addPolicy(Policy)} to add a policy.
+ */
+public interface Policy {
+
+ void onCreate(BaseTask task, Intent intent, int flags, int startId);
+
+ void onBeforeExecute();
+
+ void onCompleted();
+
+ void onFail();
+
+ void onDuplicatedTaskAdded();
+}
diff --git a/java/com/android/voicemail/impl/scheduling/PostponePolicy.java b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
new file mode 100644
index 000000000..e24df0c7a
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with Postpone policy will not be executed immediately. It will wait for a while and if a
+ * duplicated task is queued during the duration, the task will be postponed further. The task will
+ * only be executed if no new task was added in postponeMillis. Useful to batch small tasks in quick
+ * succession together.
+ */
+public class PostponePolicy implements Policy {
+
+ private static final String TAG = "PostponePolicy";
+
+ private final int mPostponeMillis;
+ private BaseTask mTask;
+
+ public PostponePolicy(int postponeMillis) {
+ mPostponeMillis = postponeMillis;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+ }
+
+ @Override
+ public void onBeforeExecute() {
+ // Do nothing
+ }
+
+ @Override
+ public void onCompleted() {
+ // Do nothing
+ }
+
+ @Override
+ public void onFail() {
+ // Do nothing
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded() {
+ if (mTask.hasStarted()) {
+ return;
+ }
+ VvmLog.d(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
new file mode 100644
index 000000000..a8e4a3d3c
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
+ * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
+ * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
+ */
+public class RetryPolicy implements Policy {
+
+ private static final String TAG = "RetryPolicy";
+ private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
+
+ private final int mRetryLimit;
+ private final int mRetryDelayMillis;
+
+ private BaseTask mTask;
+
+ private int mRetryCount;
+ private boolean mFailed;
+
+ private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
+
+ public RetryPolicy(int retryLimit, int retryDelayMillis) {
+ mRetryLimit = retryLimit;
+ mRetryDelayMillis = retryDelayMillis;
+ }
+
+ private boolean hasMoreRetries() {
+ return mRetryCount < mRetryLimit;
+ }
+
+ /**
+ * Error status should only be set if retries has exhausted or the task is successful. Status
+ * writes to this editor will be deferred until the task has ended, and will only be committed if
+ * the task is successful or there are no retries left.
+ */
+ public VoicemailStatus.Editor getVoicemailStatusEditor() {
+ return mVoicemailStatusEditor;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
+ if (mRetryCount > 0) {
+ VvmLog.d(
+ TAG,
+ "retry #" + mRetryCount + " for " + mTask + " queued, executing in " + mRetryDelayMillis);
+ mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
+ }
+ PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ VvmLog.e(TAG, "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
+ // This should never happen, but continue on if it does. The status write will be
+ // discarded.
+ }
+ mVoicemailStatusEditor = VoicemailStatus.deferredEdit(task.getContext(), phoneAccountHandle);
+ }
+
+ @Override
+ public void onBeforeExecute() {}
+
+ @Override
+ public void onCompleted() {
+ if (!mFailed || !hasMoreRetries()) {
+ if (!mFailed) {
+ VvmLog.d(TAG, mTask.toString() + " completed successfully");
+ }
+ if (!hasMoreRetries()) {
+ VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
+ }
+ VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
+ mVoicemailStatusEditor.deferredApply();
+ return;
+ }
+ VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
+ Intent intent = mTask.createRestartIntent();
+ intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
+
+ mTask.getContext().startService(intent);
+ }
+
+ @Override
+ public void onFail() {
+ mFailed = true;
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemail/impl/scheduling/Task.java b/java/com/android/voicemail/impl/scheduling/Task.java
new file mode 100644
index 000000000..2d08f5b03
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Task.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+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.
+ *
+ * <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}
+ */
+ int TASK_INVALID = -1;
+
+ /**
+ * TaskId to indicate it should always be queued regardless of duplicates. {@link
+ * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
+ */
+ int TASK_ALLOW_DUPLICATES = -2;
+
+ int TASK_UPLOAD = 1;
+ int TASK_SYNC = 2;
+ int TASK_ACTIVATION = 3;
+
+ /**
+ * Used to differentiate between types of tasks. If a task with the same TaskId is already in the
+ * queue the new task will be rejected.
+ */
+ class TaskId {
+
+ /** Indicates the operation type of the task. */
+ public final int id;
+ /**
+ * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used to
+ * differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a sync
+ * task for their own.
+ */
+ public final PhoneAccountHandle phoneAccountHandle;
+
+ public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
+ this.id = id;
+ this.phoneAccountHandle = phoneAccountHandle;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof TaskId)) {
+ return false;
+ }
+ TaskId other = (TaskId) object;
+ return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, phoneAccountHandle);
+ }
+ }
+
+ TaskId getId();
+
+ @MainThread
+ void onCreate(Context context, Intent intent, int flags, int startId);
+
+ /**
+ * @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
+ * check (it will still wake if a new task is added). The first task in the queue that is
+ * ready will be executed.
+ */
+ @MainThread
+ long getReadyInMilliSeconds();
+
+ /**
+ * Called on the main thread when the scheduler is about to send the task into the worker thread,
+ * calling {@link #onExecuteInBackgroundThread()}
+ */
+ @MainThread
+ void onBeforeExecute();
+
+ /** The actual payload of the task, executed on the worker thread. */
+ @WorkerThread
+ void onExecuteInBackgroundThread();
+
+ /**
+ * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown an
+ * uncaught exception. The task is already removed from the queue at this point, and a same task
+ * can be queued again.
+ */
+ @MainThread
+ void onCompleted();
+
+ /**
+ * Another task with the same TaskId has been added. Necessary data can be retrieved from the
+ * other task, and after this returns the task will be discarded.
+ */
+ @MainThread
+ void onDuplicatedTaskAdded(Task task);
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
new file mode 100644
index 000000000..81bd36fee
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemail.impl.scheduling;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
+ * and same task cannot exist in the queue at the same time. The service will be started when a
+ * intent is received, and stopped when there are no more tasks in the queue.
+ */
+public class TaskSchedulerService extends Service {
+
+ private static final String TAG = "VvmTaskScheduler";
+
+ private static final String ACTION_WAKEUP = "action_wakeup";
+
+ private static final int READY_TOLERANCE_MILLISECONDS = 100;
+
+ /**
+ * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
+ * future.
+ *
+ * <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 long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
+ * exact and will wake up the device. Note: as this service is run in the telephony process it
+ * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
+ * unbundled version should take doze into account.
+ */
+ private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
+ /**
+ * When there are no more tasks to be run the service should be stopped. But when all tasks has
+ * finished there might still be more tasks in the message queue waiting to be processed,
+ * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
+ * the service to make sure there are no pending messages.
+ */
+ private static final int STOP_DELAY_MILLISECONDS = 5_000;
+
+ private static final String EXTRA_CLASS_NAME = "extra_class_name";
+
+ private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
+
+ // The thread to run tasks on
+ private volatile WorkerThreadHandler mWorkerThreadHandler;
+
+ private Context mContext = this;
+ /**
+ * Used by tests to turn task handling into a single threaded process by calling {@link
+ * Handler#handleMessage(Message)} directly
+ */
+ private MessageSender mMessageSender = new MessageSender();
+
+ private MainThreadHandler mMainThreadHandler;
+
+ private WakeLock mWakeLock;
+
+ /** Main thread only, access through {@link #getTasks()} */
+ private final Queue<Task> mTasks = new ArrayDeque<>();
+
+ private boolean mWorkerThreadIsBusy = false;
+
+ private final Runnable mStopServiceWithDelay =
+ new Runnable() {
+ @Override
+ public void run() {
+ VvmLog.d(TAG, "Stopping service");
+ stopSelf();
+ }
+ };
+ /** Should attempt to run the next task when a task has finished or been added. */
+ private boolean mTaskAutoRunDisabledForTesting = 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.v(TAG, "executing task " + task);
+ task.onExecuteInBackgroundThread();
+ } catch (Throwable throwable) {
+ VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
+ }
+
+ Message schedulerMessage = mMainThreadHandler.obtainMessage();
+ schedulerMessage.obj = task;
+ mMessageSender.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();
+ mWorkerThreadIsBusy = false;
+ maybeRunNextTask();
+ }
+ }
+
+ @Override
+ @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();
+
+ mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
+ mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
+ }
+
+ @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");
+ } else {
+ Task task = createTask(intent, flags, startId);
+ if (task == null) {
+ VvmLog.e(TAG, "cannot create task form intent");
+ } else {
+ addTask(task);
+ }
+ }
+ maybeRunNextTask();
+ // STICKY means the service will be automatically restarted will the last intent if it is
+ // killed.
+ return START_NOT_STICKY;
+ }
+
+ @MainThread
+ @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) {
+ Assert.isMainThread();
+ if (intent == null) {
+ return null;
+ }
+ String className = intent.getStringExtra(EXTRA_CLASS_NAME);
+ VvmLog.d(TAG, "create task:" + className);
+ if (className == null) {
+ throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
+ }
+ try {
+ Task task = (Task) Class.forName(className).newInstance();
+ task.onCreate(mContext, intent, flags, startId);
+ return task;
+ } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @MainThread
+ private void maybeRunNextTask() {
+ Assert.isMainThread();
+ if (mWorkerThreadIsBusy) {
+ return;
+ }
+ if (mTaskAutoRunDisabledForTesting) {
+ // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
+ // to run the next task.
+ return;
+ }
+
+ runNextTask();
+ }
+
+ @VisibleForTesting
+ @MainThread
+ void runNextTask() {
+ Assert.isMainThread();
+ // The current alarm is no longer valid, a new one will be set up if required.
+ getSystemService(AlarmManager.class).cancel(getWakeupIntent());
+ if (getTasks().isEmpty()) {
+ prepareStop();
+ return;
+ }
+ Long minimalWaitTime = null;
+ for (Task task : getTasks()) {
+ long waitTime = task.getReadyInMilliSeconds();
+ if (waitTime < READY_TOLERANCE_MILLISECONDS) {
+ task.onBeforeExecute();
+ Message message = mWorkerThreadHandler.obtainMessage();
+ message.obj = task;
+ mWorkerThreadIsBusy = true;
+ mMessageSender.send(message);
+ return;
+ } else {
+ if (minimalWaitTime == null || waitTime < minimalWaitTime) {
+ minimalWaitTime = waitTime;
+ }
+ }
+ }
+ VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
+ if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
+ // No tasks are currently ready. Sleep until the next one should be.
+ // If a new task is added during the sleep the service will wake immediately.
+ sleep(minimalWaitTime);
+ }
+ }
+
+ private void sleep(long timeMillis) {
+ if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
+ mMainThreadHandler.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ maybeRunNextTask();
+ }
+ },
+ timeMillis);
+ return;
+ }
+
+ // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
+ // optimize the battery usage. As this service currently run in the telephony process the
+ // OS give it privileges to behave the same as setExact(), but set() is the targeted
+ // behavior once this is unbundled.
+ getSystemService(AlarmManager.class)
+ .set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + timeMillis,
+ getWakeupIntent());
+ mWakeLock.release();
+ VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
+ }
+
+ private PendingIntent getWakeupIntent() {
+ Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
+ return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ private void prepareStop() {
+ VvmLog.d(
+ TAG,
+ "No more tasks, stopping service if no task are added in "
+ + STOP_DELAY_MILLISECONDS
+ + " millis");
+ mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
+ }
+
+ static class MessageSender {
+
+ public void send(Message message) {
+ message.sendToTarget();
+ }
+ }
+
+ @NeededForTesting
+ void setContextForTest(Context context) {
+ mContext = context;
+ }
+
+ @NeededForTesting
+ void setTaskAutoRunDisabledForTest(boolean value) {
+ mTaskAutoRunDisabledForTesting = value;
+ }
+
+ @NeededForTesting
+ void setMessageSenderForTest(MessageSender sender) {
+ mMessageSender = sender;
+ }
+
+ @NeededForTesting
+ void clearTasksForTest() {
+ mTasks.clear();
+ }
+
+ @Override
+ @Nullable
+ public IBinder onBind(Intent intent) {
+ return new LocalBinder();
+ }
+
+ @NeededForTesting
+ class LocalBinder extends Binder {
+
+ @NeededForTesting
+ public TaskSchedulerService getService() {
+ return TaskSchedulerService.this;
+ }
+ }
+}