diff options
author | Eric Erfanian <erfanian@google.com> | 2017-05-03 23:10:39 +0000 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2017-05-03 23:10:39 +0000 |
commit | d67f1627521b174f12e0c247f154c7c93c5e408e (patch) | |
tree | 1a45d60921e293c6088efeaf4d9c408456f3e0e2 /java/com/android/voicemail/impl/scheduling | |
parent | 010981b798fbc6b8e6a00c3ea507f00c3e77c371 (diff) | |
parent | 8369df095a73a77b3715f8ae7ba06089cebca4ce (diff) |
This change reflects the Dialer V10 RC00 branch.
am: 8369df095a
Change-Id: I6820870feb74cba010ac3039e4da41325b3005f9
Diffstat (limited to 'java/com/android/voicemail/impl/scheduling')
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; + } +} |