diff options
Diffstat (limited to 'java/com/android/voicemailomtp/scheduling')
8 files changed, 1077 insertions, 0 deletions
diff --git a/java/com/android/voicemailomtp/scheduling/BaseTask.java b/java/com/android/voicemailomtp/scheduling/BaseTask.java new file mode 100644 index 000000000..8097bb4dc --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/BaseTask.java @@ -0,0 +1,206 @@ +/* + * 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.voicemailomtp.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 android.telephony.SubscriptionManager; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.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/voicemailomtp/scheduling/BlockerTask.java b/java/com/android/voicemailomtp/scheduling/BlockerTask.java new file mode 100644 index 000000000..55ad9a7fd --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/BlockerTask.java @@ -0,0 +1,55 @@ +/* + * 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.voicemailomtp.scheduling; + +import android.content.Context; +import android.content.Intent; + +import com.android.voicemailomtp.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/voicemailomtp/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java new file mode 100644 index 000000000..bef449b30 --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java @@ -0,0 +1,69 @@ +/* + * 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.voicemailomtp.scheduling; + +import android.content.Intent; + +import com.android.voicemailomtp.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/voicemailomtp/scheduling/Policy.java b/java/com/android/voicemailomtp/scheduling/Policy.java new file mode 100644 index 000000000..4a475d2ed --- /dev/null +++ b/java/com/android/voicemailomtp/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.voicemailomtp.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/voicemailomtp/scheduling/PostponePolicy.java b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java new file mode 100644 index 000000000..27a82f0ef --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java @@ -0,0 +1,69 @@ +/* + * 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.voicemailomtp.scheduling; + +import android.content.Intent; + +import com.android.voicemailomtp.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/voicemailomtp/scheduling/RetryPolicy.java b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java new file mode 100644 index 000000000..463657483 --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java @@ -0,0 +1,117 @@ +/* + * 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.voicemailomtp.scheduling; + +import android.content.Intent; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.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/voicemailomtp/scheduling/Task.java b/java/com/android/voicemailomtp/scheduling/Task.java new file mode 100644 index 000000000..61c35396b --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/Task.java @@ -0,0 +1,133 @@ +/* + * 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.voicemailomtp.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/voicemailomtp/scheduling/TaskSchedulerService.java b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java new file mode 100644 index 000000000..90b50e913 --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java @@ -0,0 +1,392 @@ +/* + * 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.voicemailomtp.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.voicemailomtp.Assert; +import com.android.voicemailomtp.NeededForTesting; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.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; + } + } +} |