diff options
author | Christopher Tate <ctate@android.com> | 2014-05-29 21:17:32 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2014-05-29 21:17:32 +0000 |
commit | 3d5e5c7b266863953ece3d77ab2c334e9a9c4ebc (patch) | |
tree | d207a9816973cb82b776eccdf045289185eb571c /services | |
parent | 073c2e4ad1437bcb97b5290ad31ab40ef309716b (diff) | |
parent | 9b9244b6941110ea2d940d9fc8eed0cdff96a016 (diff) | |
download | frameworks_base-3d5e5c7b266863953ece3d77ab2c334e9a9c4ebc.zip frameworks_base-3d5e5c7b266863953ece3d77ab2c334e9a9c4ebc.tar.gz frameworks_base-3d5e5c7b266863953ece3d77ab2c334e9a9c4ebc.tar.bz2 |
Merge "DO NOT MERGE Implementation of TaskManager reschedule/cancel" into lmp-preview-dev
Diffstat (limited to 'services')
10 files changed, 908 insertions, 517 deletions
diff --git a/services/core/java/com/android/server/task/StateChangedListener.java b/services/core/java/com/android/server/task/StateChangedListener.java index db2d4ee..b1a4636 100644 --- a/services/core/java/com/android/server/task/StateChangedListener.java +++ b/services/core/java/com/android/server/task/StateChangedListener.java @@ -27,9 +27,8 @@ public interface StateChangedListener { /** * Called by the controller to notify the TaskManager that it should check on the state of a * task. - * @param taskStatus The state of the task which has changed. */ - public void onTaskStateChanged(TaskStatus taskStatus); + public void onControllerStateChanged(); /** * Called by the controller to notify the TaskManager that regardless of the state of the task, diff --git a/services/core/java/com/android/server/task/TaskCompletedListener.java b/services/core/java/com/android/server/task/TaskCompletedListener.java index 0210442..c53f5ca 100644 --- a/services/core/java/com/android/server/task/TaskCompletedListener.java +++ b/services/core/java/com/android/server/task/TaskCompletedListener.java @@ -16,6 +16,8 @@ package com.android.server.task; +import com.android.server.task.controllers.TaskStatus; + /** * Used for communication between {@link com.android.server.task.TaskServiceContext} and the * {@link com.android.server.task.TaskManagerService}. @@ -26,13 +28,5 @@ public interface TaskCompletedListener { * Callback for when a task is completed. * @param needsReschedule Whether the implementing class should reschedule this task. */ - public void onTaskCompleted(int serviceToken, int taskId, boolean needsReschedule); - - /** - * Callback for when the implementing class needs to clean up the - * {@link com.android.server.task.TaskServiceContext}. The scheduler can get this callback - * several times if the TaskServiceContext got into a bad state (for e.g. the client crashed - * and it needs to clean up). - */ - public void onAllTasksCompleted(int serviceToken); + public void onTaskCompleted(TaskStatus taskStatus, boolean needsReschedule); } diff --git a/services/core/java/com/android/server/task/TaskManagerService.java b/services/core/java/com/android/server/task/TaskManagerService.java index 80030b4..d5b70e6 100644 --- a/services/core/java/com/android/server/task/TaskManagerService.java +++ b/services/core/java/com/android/server/task/TaskManagerService.java @@ -19,10 +19,12 @@ package com.android.server.task; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import android.app.task.ITaskManager; import android.app.task.Task; +import android.app.task.TaskManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; @@ -30,11 +32,17 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; -import android.os.UserHandle; +import android.os.SystemClock; import android.util.Slog; import android.util.SparseArray; +import com.android.server.task.controllers.ConnectivityController; +import com.android.server.task.controllers.IdleController; +import com.android.server.task.controllers.StateController; import com.android.server.task.controllers.TaskStatus; +import com.android.server.task.controllers.TimeController; + +import java.util.LinkedList; /** * Responsible for taking tasks representing work to be performed by a client app, and determining @@ -44,61 +52,148 @@ import com.android.server.task.controllers.TaskStatus; */ public class TaskManagerService extends com.android.server.SystemService implements StateChangedListener, TaskCompletedListener { + // TODO: Switch this off for final version. + private static final boolean DEBUG = true; + /** The number of concurrent tasks we run at one time. */ + private static final int MAX_TASK_CONTEXTS_COUNT = 3; static final String TAG = "TaskManager"; + /** + * When a task fails, it gets rescheduled according to its backoff policy. To be nice, we allow + * this amount of time from the rescheduled time by which the retry must occur. + */ + private static final long RESCHEDULE_WINDOW_SLOP_MILLIS = 5000L; /** Master list of tasks. */ private final TaskStore mTasks; + static final int MSG_TASK_EXPIRED = 0; + static final int MSG_CHECK_TASKS = 1; + + // Policy constants + /** + * Minimum # of idle tasks that must be ready in order to force the TM to schedule things + * early. + */ + private static final int MIN_IDLE_COUNT = 1; + /** + * Minimum # of connectivity tasks that must be ready in order to force the TM to schedule + * things early. + */ + private static final int MIN_CONNECTIVITY_COUNT = 2; + /** + * Minimum # of tasks (with no particular constraints) for which the TM will be happy running + * some work early. + */ + private static final int MIN_READY_TASKS_COUNT = 4; + /** * Track Services that have currently active or pending tasks. The index is provided by * {@link TaskStatus#getServiceToken()} */ - private final SparseArray<TaskServiceContext> mActiveServices = - new SparseArray<TaskServiceContext>(); + private final List<TaskServiceContext> mActiveServices = new LinkedList<TaskServiceContext>(); + /** List of controllers that will notify this service of updates to tasks. */ + private List<StateController> mControllers; + /** + * Queue of pending tasks. The TaskServiceContext class will receive tasks from this list + * when ready to execute them. + */ + private final LinkedList<TaskStatus> mPendingTasks = new LinkedList<TaskStatus>(); private final TaskHandler mHandler; private final TaskManagerStub mTaskManagerStub; - /** Check the pending queue and start any tasks. */ - static final int MSG_RUN_PENDING = 0; - /** Initiate the stop task flow. */ - static final int MSG_STOP_TASK = 1; - /** */ - static final int MSG_CHECK_TASKS = 2; - - private class TaskHandler extends Handler { + /** + * Entry point from client to schedule the provided task. + * This will add the task to the + * @param task Task object containing execution parameters + * @param uId The package identifier of the application this task is for. + * @param canPersistTask Whether or not the client has the appropriate permissions for persisting + * of this task. + * @return Result of this operation. See <code>TaskManager#RESULT_*</code> return codes. + */ + public int schedule(Task task, int uId, boolean canPersistTask) { + TaskStatus taskStatus = new TaskStatus(task, uId, canPersistTask); + return startTrackingTask(taskStatus) ? + TaskManager.RESULT_SUCCESS : TaskManager.RESULT_FAILURE; + } - public TaskHandler(Looper looper) { - super(looper); + public List<Task> getPendingTasks(int uid) { + ArrayList<Task> outList = new ArrayList<Task>(); + synchronized (mTasks) { + for (TaskStatus ts : mTasks.getTasks()) { + if (ts.getUid() == uid) { + outList.add(ts.getTask()); + } + } } + return outList; + } - @Override - public void handleMessage(Message message) { - switch (message.what) { - case MSG_RUN_PENDING: - - break; - case MSG_STOP_TASK: - - break; - case MSG_CHECK_TASKS: - checkTasks(); - break; + /** + * Entry point from client to cancel all tasks originating from their uid. + * This will remove the task from the master list, and cancel the task if it was staged for + * execution or being executed. + * @param uid To check against for removal of a task. + */ + public void cancelTaskForUid(int uid) { + // Remove from master list. + synchronized (mTasks) { + if (!mTasks.removeAllByUid(uid)) { + // If it's not in the master list, it's nowhere. + return; } } + // Remove from pending queue. + synchronized (mPendingTasks) { + Iterator<TaskStatus> it = mPendingTasks.iterator(); + while (it.hasNext()) { + TaskStatus ts = it.next(); + if (ts.getUid() == uid) { + it.remove(); + } + } + } + // Cancel if running. + synchronized (mActiveServices) { + for (TaskServiceContext tsc : mActiveServices) { + if (tsc.getRunningTask().getUid() == uid) { + tsc.cancelExecutingTask(); + } + } + } + } - /** - * Called when we need to run through the list of all tasks and start/stop executing one or - * more of them. - */ - private void checkTasks() { - synchronized (mTasks) { - final SparseArray<TaskStatus> tasks = mTasks.getTasks(); - for (int i = 0; i < tasks.size(); i++) { - TaskStatus ts = tasks.valueAt(i); - if (ts.isReady() && ! isCurrentlyActive(ts)) { - assignTaskToServiceContext(ts); - } + /** + * Entry point from client to cancel the task corresponding to the taskId provided. + * This will remove the task from the master list, and cancel the task if it was staged for + * execution or being executed. + * @param uid Uid of the calling client. + * @param taskId Id of the task, provided at schedule-time. + */ + public void cancelTask(int uid, int taskId) { + synchronized (mTasks) { + if (!mTasks.remove(uid, taskId)) { + // If it's not in the master list, it's nowhere. + return; + } + } + synchronized (mPendingTasks) { + Iterator<TaskStatus> it = mPendingTasks.iterator(); + while (it.hasNext()) { + TaskStatus ts = it.next(); + if (ts.getUid() == uid && ts.getTaskId() == taskId) { + it.remove(); + // If we got it from pending, it didn't make it to active so return. + return; + } + } + } + synchronized (mActiveServices) { + for (TaskServiceContext tsc : mActiveServices) { + if (tsc.getRunningTask().getUid() == uid && + tsc.getRunningTask().getTaskId() == taskId) { + tsc.cancelExecutingTask(); + return; } } } @@ -118,6 +213,17 @@ public class TaskManagerService extends com.android.server.SystemService mTasks = new TaskStore(context); mHandler = new TaskHandler(context.getMainLooper()); mTaskManagerStub = new TaskManagerStub(); + // Create the "runners". + for (int i = 0; i < MAX_TASK_CONTEXTS_COUNT; i++) { + mActiveServices.add( + new TaskServiceContext(this, context.getMainLooper())); + } + + mControllers = new LinkedList<StateController>(); + mControllers.add(ConnectivityController.get(this)); + mControllers.add(TimeController.get(this)); + mControllers.add(IdleController.get(this)); + // TODO: Add BatteryStateController when implemented. } @Override @@ -126,53 +232,128 @@ public class TaskManagerService extends com.android.server.SystemService } /** - * Entry point from client to schedule the provided task. - * This will add the task to the - * @param task Task object containing execution parameters - * @param userId The id of the user this task is for. - * @param uId The package identifier of the application this task is for. - * @param canPersistTask Whether or not the client has the appropriate permissions for - * persisting of this task. - * @return Result of this operation. See <code>TaskManager#RESULT_*</code> return codes. + * Called when we have a task status object that we need to insert in our + * {@link com.android.server.task.TaskStore}, and make sure all the relevant controllers know + * about. */ - public int schedule(Task task, int userId, int uId, boolean canPersistTask) { - TaskStatus taskStatus = mTasks.addNewTaskForUser(task, userId, uId, canPersistTask); - return 0; + private boolean startTrackingTask(TaskStatus taskStatus) { + boolean added = false; + synchronized (mTasks) { + added = mTasks.add(taskStatus); + } + if (added) { + for (StateController controller : mControllers) { + controller.maybeStartTrackingTask(taskStatus); + } + } + return added; } - public List<Task> getPendingTasks(int uid) { - ArrayList<Task> outList = new ArrayList<Task>(3); + /** + * Called when we want to remove a TaskStatus object that we've finished executing. Returns the + * object removed. + */ + private boolean stopTrackingTask(TaskStatus taskStatus) { + boolean removed; synchronized (mTasks) { - final SparseArray<TaskStatus> tasks = mTasks.getTasks(); - final int N = tasks.size(); - for (int i = 0; i < N; i++) { - TaskStatus ts = tasks.get(i); - if (ts.getUid() == uid) { - outList.add(ts.getTask()); - } + // Remove from store as well as controllers. + removed = mTasks.remove(taskStatus); + } + if (removed) { + for (StateController controller : mControllers) { + controller.maybeStopTrackingTask(taskStatus); } } - return outList; + return removed; } - // StateChangedListener implementations. + private boolean cancelTaskOnServiceContext(TaskStatus ts) { + synchronized (mActiveServices) { + for (TaskServiceContext tsc : mActiveServices) { + if (tsc.getRunningTask() == ts) { + tsc.cancelExecutingTask(); + return true; + } + } + return false; + } + } /** - * Off-board work to our handler thread as quickly as possible, b/c this call is probably being - * made on the main thread. - * For now this takes the task and if it's ready to run it will run it. In future we might not - * provide the task, so that the StateChangedListener has to run through its list of tasks to - * see which are ready. This will further decouple the controllers from the execution logic. + * @param ts TaskStatus we are querying against. + * @return Whether or not the task represented by the status object is currently being run or + * is pending. */ - @Override - public void onTaskStateChanged(TaskStatus taskStatus) { - postCheckTasksMessage(); - + private boolean isCurrentlyActive(TaskStatus ts) { + synchronized (mActiveServices) { + for (TaskServiceContext serviceContext : mActiveServices) { + if (serviceContext.getRunningTask() == ts) { + return true; + } + } + return false; + } } - @Override - public void onTaskDeadlineExpired(TaskStatus taskStatus) { + /** + * A task is rescheduled with exponential back-off if the client requests this from their + * execution logic. + * A caveat is for idle-mode tasks, for which the idle-mode constraint will usurp the + * timeliness of the reschedule. For an idle-mode task, no deadline is given. + * @param failureToReschedule Provided task status that we will reschedule. + * @return A newly instantiated TaskStatus with the same constraints as the last task except + * with adjusted timing constraints. + */ + private TaskStatus getRescheduleTaskForFailure(TaskStatus failureToReschedule) { + final long elapsedNowMillis = SystemClock.elapsedRealtime(); + final Task task = failureToReschedule.getTask(); + + final long initialBackoffMillis = task.getInitialBackoffMillis(); + final int backoffAttempt = failureToReschedule.getNumFailures() + 1; + long newEarliestRuntimeElapsed = elapsedNowMillis; + + switch (task.getBackoffPolicy()) { + case Task.BackoffPolicy.LINEAR: + newEarliestRuntimeElapsed += initialBackoffMillis * backoffAttempt; + break; + default: + if (DEBUG) { + Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential."); + } + case Task.BackoffPolicy.EXPONENTIAL: + newEarliestRuntimeElapsed += Math.pow(initialBackoffMillis, backoffAttempt); + break; + } + long newLatestRuntimeElapsed = failureToReschedule.hasIdleConstraint() ? Long.MAX_VALUE + : newEarliestRuntimeElapsed + RESCHEDULE_WINDOW_SLOP_MILLIS; + return new TaskStatus(failureToReschedule, newEarliestRuntimeElapsed, + newLatestRuntimeElapsed, backoffAttempt); + } + /** + * Called after a periodic has executed so we can to re-add it. We take the last execution time + * of the task to be the time of completion (i.e. the time at which this function is called). + * This could be inaccurate b/c the task can run for as long as + * {@link com.android.server.task.TaskServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead + * to underscheduling at least, rather than if we had taken the last execution time to be the + * start of the execution. + * @return A new task representing the execution criteria for this instantiation of the + * recurring task. + */ + private TaskStatus getRescheduleTaskForPeriodic(TaskStatus periodicToReschedule) { + final long elapsedNow = SystemClock.elapsedRealtime(); + // Compute how much of the period is remaining. + long runEarly = Math.max(periodicToReschedule.getLatestRunTimeElapsed() - elapsedNow, 0); + long newEarliestRunTimeElapsed = elapsedNow + runEarly; + long period = periodicToReschedule.getTask().getIntervalMillis(); + long newLatestRuntimeElapsed = newEarliestRunTimeElapsed + period; + + if (DEBUG) { + Slog.v(TAG, "Rescheduling executed periodic. New execution window [" + + newEarliestRunTimeElapsed/1000 + ", " + newLatestRuntimeElapsed/1000 + "]s"); + } + return new TaskStatus(periodicToReschedule, newEarliestRunTimeElapsed, + newLatestRuntimeElapsed, 0 /* backoffAttempt */); } // TaskCompletedListener implementations. @@ -181,54 +362,155 @@ public class TaskManagerService extends com.android.server.SystemService * A task just finished executing. We fetch the * {@link com.android.server.task.controllers.TaskStatus} from the store and depending on * whether we want to reschedule we readd it to the controllers. - * @param serviceToken key for the service context in {@link #mActiveServices}. - * @param taskId Id of the task that is complete. + * @param taskStatus Completed task. * @param needsReschedule Whether the implementing class should reschedule this task. */ @Override - public void onTaskCompleted(int serviceToken, int taskId, boolean needsReschedule) { - final TaskServiceContext serviceContext = mActiveServices.get(serviceToken); - if (serviceContext == null) { - Slog.e(TAG, "Task completed for invalid service context; " + serviceToken); + public void onTaskCompleted(TaskStatus taskStatus, boolean needsReschedule) { + if (!stopTrackingTask(taskStatus)) { + if (DEBUG) { + Slog.e(TAG, "Error removing task: could not find task to remove. Was task" + + "removed while executing?"); + } return; } + if (needsReschedule) { + TaskStatus rescheduled = getRescheduleTaskForFailure(taskStatus); + startTrackingTask(rescheduled); + } else if (taskStatus.getTask().isPeriodic()) { + TaskStatus rescheduledPeriodic = getRescheduleTaskForPeriodic(taskStatus); + startTrackingTask(rescheduledPeriodic); + } + mHandler.obtainMessage(MSG_CHECK_TASKS).sendToTarget(); + } + + // StateChangedListener implementations. + /** + * Off-board work to our handler thread as quickly as possible, b/c this call is probably being + * made on the main thread. + * For now this takes the task and if it's ready to run it will run it. In future we might not + * provide the task, so that the StateChangedListener has to run through its list of tasks to + * see which are ready. This will further decouple the controllers from the execution logic. + */ + @Override + public void onControllerStateChanged() { + // Post a message to to run through the list of tasks and start/stop any that are eligible. + mHandler.obtainMessage(MSG_CHECK_TASKS).sendToTarget(); } @Override - public void onAllTasksCompleted(int serviceToken) { - + public void onTaskDeadlineExpired(TaskStatus taskStatus) { + mHandler.obtainMessage(MSG_TASK_EXPIRED, taskStatus); } - private void assignTaskToServiceContext(TaskStatus ts) { - TaskServiceContext serviceContext = - mActiveServices.get(ts.getServiceToken()); - if (serviceContext == null) { - serviceContext = new TaskServiceContext(this, mHandler.getLooper(), ts); - mActiveServices.put(ts.getServiceToken(), serviceContext); + private class TaskHandler extends Handler { + + public TaskHandler(Looper looper) { + super(looper); } - serviceContext.addPendingTask(ts); - } - /** - * @param ts TaskStatus we are querying against. - * @return Whether or not the task represented by the status object is currently being run or - * is pending. - */ - private boolean isCurrentlyActive(TaskStatus ts) { - TaskServiceContext serviceContext = mActiveServices.get(ts.getServiceToken()); - if (serviceContext == null) { - return false; + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TASK_EXPIRED: + final TaskStatus expired = (TaskStatus) message.obj; // Unused for now. + queueReadyTasksForExecutionH(); + break; + case MSG_CHECK_TASKS: + // Check the list of tasks and run some of them if we feel inclined. + maybeQueueReadyTasksForExecutionH(); + break; + } + maybeRunNextPendingTaskH(); + // Don't remove TASK_EXPIRED in case one came along while processing the queue. + removeMessages(MSG_CHECK_TASKS); } - return serviceContext.hasTaskPending(ts); - } - /** - * Post a message to {@link #mHandler} to run through the list of tasks and start/stop any that - * are eligible. - */ - private void postCheckTasksMessage() { - mHandler.obtainMessage(MSG_CHECK_TASKS).sendToTarget(); + /** + * Run through list of tasks and execute all possible - at least one is expired so we do + * as many as we can. + */ + private void queueReadyTasksForExecutionH() { + synchronized (mTasks) { + for (TaskStatus ts : mTasks.getTasks()) { + final boolean criteriaSatisfied = ts.isReady(); + final boolean isRunning = isCurrentlyActive(ts); + if (criteriaSatisfied && !isRunning) { + synchronized (mPendingTasks) { + mPendingTasks.add(ts); + } + } else if (!criteriaSatisfied && isRunning) { + cancelTaskOnServiceContext(ts); + } + } + } + } + + /** + * The state of at least one task has changed. Here is where we could enforce various + * policies on when we want to execute tasks. + * Right now the policy is such: + * If >1 of the ready tasks is idle mode we send all of them off + * if more than 2 network connectivity tasks are ready we send them all off. + * If more than 4 tasks total are ready we send them all off. + * TODO: It would be nice to consolidate these sort of high-level policies somewhere. + */ + private void maybeQueueReadyTasksForExecutionH() { + synchronized (mTasks) { + int idleCount = 0; + int connectivityCount = 0; + List<TaskStatus> runnableTasks = new ArrayList<TaskStatus>(); + for (TaskStatus ts : mTasks.getTasks()) { + final boolean criteriaSatisfied = ts.isReady(); + final boolean isRunning = isCurrentlyActive(ts); + if (criteriaSatisfied && !isRunning) { + if (ts.hasIdleConstraint()) { + idleCount++; + } + if (ts.hasConnectivityConstraint() || ts.hasMeteredConstraint()) { + connectivityCount++; + } + runnableTasks.add(ts); + } else if (!criteriaSatisfied && isRunning) { + cancelTaskOnServiceContext(ts); + } + } + if (idleCount >= MIN_IDLE_COUNT || connectivityCount >= MIN_CONNECTIVITY_COUNT || + runnableTasks.size() >= MIN_READY_TASKS_COUNT) { + for (TaskStatus ts : runnableTasks) { + synchronized (mPendingTasks) { + mPendingTasks.add(ts); + } + } + } + } + } + + /** + * Checks the state of the pending queue against any available + * {@link com.android.server.task.TaskServiceContext} that can run a new task. + * {@link com.android.server.task.TaskServiceContext}. + */ + private void maybeRunNextPendingTaskH() { + TaskStatus nextPending; + synchronized (mPendingTasks) { + nextPending = mPendingTasks.poll(); + } + if (nextPending == null) { + return; + } + + synchronized (mActiveServices) { + for (TaskServiceContext tsc : mActiveServices) { + if (tsc.isAvailable()) { + if (tsc.executeRunnableTask(nextPending)) { + return; + } + } + } + } + } } /** @@ -268,11 +550,10 @@ public class TaskManagerService extends com.android.server.SystemService public int schedule(Task task) throws RemoteException { final boolean canPersist = canCallerPersistTasks(); final int uid = Binder.getCallingUid(); - final int userId = UserHandle.getCallingUserId(); long ident = Binder.clearCallingIdentity(); try { - return TaskManagerService.this.schedule(task, userId, uid, canPersist); + return TaskManagerService.this.schedule(task, uid, canPersist); } finally { Binder.restoreCallingIdentity(ident); } @@ -280,15 +561,38 @@ public class TaskManagerService extends com.android.server.SystemService @Override public List<Task> getAllPendingTasks() throws RemoteException { - return null; + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + return TaskManagerService.this.getPendingTasks(uid); + } finally { + Binder.restoreCallingIdentity(ident); + } } @Override public void cancelAll() throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + TaskManagerService.this.cancelTaskForUid(uid); + } finally { + Binder.restoreCallingIdentity(ident); + } } @Override public void cancel(int taskId) throws RemoteException { + final int uid = Binder.getCallingUid(); + + long ident = Binder.clearCallingIdentity(); + try { + TaskManagerService.this.cancelTask(uid, taskId); + } finally { + Binder.restoreCallingIdentity(ident); + } } /** @@ -311,9 +615,7 @@ public class TaskManagerService extends com.android.server.SystemService synchronized (mTasks) { pw.print("Registered tasks:"); if (mTasks.size() > 0) { - SparseArray<TaskStatus> tasks = mTasks.getTasks(); - for (int i = 0; i < tasks.size(); i++) { - TaskStatus ts = tasks.get(i); + for (TaskStatus ts : mTasks.getTasks()) { pw.println(); ts.dump(pw, " "); } diff --git a/services/core/java/com/android/server/task/TaskServiceContext.java b/services/core/java/com/android/server/task/TaskServiceContext.java index 165445a..75e9212 100644 --- a/services/core/java/com/android/server/task/TaskServiceContext.java +++ b/services/core/java/com/android/server/task/TaskServiceContext.java @@ -24,6 +24,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -36,20 +37,18 @@ import android.util.Log; import android.util.Slog; import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.task.controllers.TaskStatus; import java.util.concurrent.atomic.AtomicBoolean; /** - * Maintains information required to bind to a {@link android.app.task.TaskService}. This binding - * is reused to start concurrent tasks on the TaskService. Information here is unique - * to the service. - * Functionality provided by this class: - * - Manages wakelock for the service. - * - Sends onStartTask() and onStopTask() messages to client app, and handles callbacks. - * - + * Handles client binding and lifecycle of a task. A task will only execute one at a time on an + * instance of this class. */ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceConnection { + private static final boolean DEBUG = true; private static final String TAG = "TaskServiceContext"; /** Define the maximum # of tasks allowed to run on a service at once. */ private static final int defaultMaxActiveTasksPerService = @@ -66,10 +65,10 @@ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceCon }; // States that a task occupies while interacting with the client. - private static final int VERB_STARTING = 0; - private static final int VERB_EXECUTING = 1; - private static final int VERB_STOPPING = 2; - private static final int VERB_PENDING = 3; + static final int VERB_BINDING = 0; + static final int VERB_STARTING = 1; + static final int VERB_EXECUTING = 2; + static final int VERB_STOPPING = 3; // Messages that result from interactions with the client service. /** System timed out waiting for a response. */ @@ -77,178 +76,173 @@ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceCon /** Received a callback from client. */ private static final int MSG_CALLBACK = 1; /** Run through list and start any ready tasks.*/ - private static final int MSG_CHECK_PENDING = 2; - /** Cancel an active task. */ + private static final int MSG_SERVICE_BOUND = 2; + /** Cancel a task. */ private static final int MSG_CANCEL = 3; - /** Add a pending task. */ - private static final int MSG_ADD_PENDING = 4; - /** Client crashed, so we need to wind things down. */ - private static final int MSG_SHUTDOWN = 5; - - /** Used to identify this task service context when communicating with the TaskManager. */ - final int token; - final ComponentName component; - final int userId; - ITaskService service; + /** Shutdown the Task. Used when the client crashes and we can't die gracefully.*/ + private static final int MSG_SHUTDOWN_EXECUTION = 4; + private final Handler mCallbackHandler; - /** Tasks that haven't been sent to the client for execution yet. */ - private final SparseArray<ActiveTask> mPending; + /** Make callbacks to {@link TaskManagerService} to inform on task completion status. */ + private final TaskCompletedListener mCompletedListener; /** Used for service binding, etc. */ private final Context mContext; - /** Make callbacks to {@link TaskManagerService} to inform on task completion status. */ - final private TaskCompletedListener mCompletedListener; - private final PowerManager.WakeLock mWakeLock; + private PowerManager.WakeLock mWakeLock; - /** Whether this service is actively bound. */ - boolean mBound; + // Execution state. + private TaskParams mParams; + @VisibleForTesting + int mVerb; + private AtomicBoolean mCancelled = new AtomicBoolean(); - TaskServiceContext(TaskManagerService taskManager, Looper looper, TaskStatus taskStatus) { - mContext = taskManager.getContext(); - this.component = taskStatus.getServiceComponent(); - this.token = taskStatus.getServiceToken(); - this.userId = taskStatus.getUserId(); - mCallbackHandler = new TaskServiceHandler(looper); - mPending = new SparseArray<ActiveTask>(); - mCompletedListener = taskManager; - final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - TM_WAKELOCK_PREFIX + component.getPackageName()); - mWakeLock.setWorkSource(new WorkSource(taskStatus.getUid())); - mWakeLock.setReferenceCounted(false); - } + /** All the information maintained about the task currently being executed. */ + private TaskStatus mRunningTask; + /** Binder to the client service. */ + ITaskService service; - @Override - public void taskFinished(int taskId, boolean reschedule) { - mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0) - .sendToTarget(); - } + private final Object mAvailableLock = new Object(); + /** Whether this context is free. */ + @GuardedBy("mAvailableLock") + private boolean mAvailable; - @Override - public void acknowledgeStopMessage(int taskId, boolean reschedule) { - mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0) - .sendToTarget(); + TaskServiceContext(TaskManagerService service, Looper looper) { + this(service.getContext(), service, looper); } - @Override - public void acknowledgeStartMessage(int taskId, boolean ongoing) { - mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, ongoing ? 1 : 0).sendToTarget(); + @VisibleForTesting + TaskServiceContext(Context context, TaskCompletedListener completedListener, Looper looper) { + mContext = context; + mCallbackHandler = new TaskServiceHandler(looper); + mCompletedListener = completedListener; } /** - * Queue up this task to run on the client. This will execute the task as quickly as possible. - * @param ts Status of the task to run. + * Give a task to this context for execution. Callers must first check {@link #isAvailable()} + * to make sure this is a valid context. + * @param ts The status of the task that we are going to run. + * @return True if the task was accepted and is going to run. */ - public void addPendingTask(TaskStatus ts) { - final TaskParams params = new TaskParams(ts.getTaskId(), ts.getExtras(), this); - final ActiveTask newTask = new ActiveTask(params, VERB_PENDING); - mCallbackHandler.obtainMessage(MSG_ADD_PENDING, newTask).sendToTarget(); - if (!mBound) { - Intent intent = new Intent().setComponent(component); - boolean binding = mContext.bindServiceAsUser(intent, this, - Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND, - new UserHandle(userId)); - if (!binding) { - Log.e(TAG, component.getShortClassName() + " unavailable."); - cancelPendingTask(ts); + boolean executeRunnableTask(TaskStatus ts) { + synchronized (mAvailableLock) { + if (!mAvailable) { + Slog.e(TAG, "Starting new runnable but context is unavailable > Error."); + return false; + } + mAvailable = false; + } + + final PowerManager pm = + (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + TM_WAKELOCK_PREFIX + ts.getServiceComponent().getPackageName()); + mWakeLock.setWorkSource(new WorkSource(ts.getUid())); + mWakeLock.setReferenceCounted(false); + + mRunningTask = ts; + mParams = new TaskParams(ts.getTaskId(), ts.getExtras(), this); + + mVerb = VERB_BINDING; + final Intent intent = new Intent().setComponent(ts.getServiceComponent()); + boolean binding = mContext.bindServiceAsUser(intent, this, + Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND, + new UserHandle(ts.getUserId())); + if (!binding) { + if (DEBUG) { + Slog.d(TAG, ts.getServiceComponent().getShortClassName() + " unavailable."); } + return false; } + + return true; } - /** - * Called externally when a task that was scheduled for execution should be cancelled. - * @param ts The status of the task to cancel. - */ - public void cancelPendingTask(TaskStatus ts) { - mCallbackHandler.obtainMessage(MSG_CANCEL, ts.getTaskId(), -1 /* arg2 */) - .sendToTarget(); + /** Used externally to query the running task. Will return null if there is no task running. */ + TaskStatus getRunningTask() { + return mRunningTask; + } + + /** Called externally when a task that was scheduled for execution should be cancelled. */ + void cancelExecutingTask() { + mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget(); } /** - * MSG_TIMEOUT is sent with the {@link com.android.server.task.TaskServiceContext.ActiveTask} - * set in the {@link Message#obj} field. This makes it easier to remove timeouts for a given - * ActiveTask. - * @param op Operation that is taking place. + * @return Whether this context is available to handle incoming work. */ - private void scheduleOpTimeOut(ActiveTask op) { - mCallbackHandler.removeMessages(MSG_TIMEOUT, op); - - final long timeoutMillis = (op.verb == VERB_EXECUTING) ? - EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS; - if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) { - Slog.d(TAG, "Scheduling time out for '" + component.getShortClassName() + "' tId: " + - op.params.getTaskId() + ", in " + (timeoutMillis / 1000) + " s"); + boolean isAvailable() { + synchronized (mAvailableLock) { + return mAvailable; } - Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT, op); - mCallbackHandler.sendMessageDelayed(m, timeoutMillis); } - /** - * @return true if this task is pending or active within this context. - */ - public boolean hasTaskPending(TaskStatus taskStatus) { - synchronized (mPending) { - return mPending.get(taskStatus.getTaskId()) != null; + @Override + public void taskFinished(int taskId, boolean reschedule) { + if (!verifyCallingUid()) { + return; + } + mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0) + .sendToTarget(); + } + + @Override + public void acknowledgeStopMessage(int taskId, boolean reschedule) { + if (!verifyCallingUid()) { + return; } + mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0) + .sendToTarget(); } - public boolean isBound() { - return mBound; + @Override + public void acknowledgeStartMessage(int taskId, boolean ongoing) { + if (!verifyCallingUid()) { + return; + } + mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, ongoing ? 1 : 0).sendToTarget(); } /** - * We acquire/release the wakelock on onServiceConnected/unbindService. This mirrors the work + * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work * we intend to send to the client - we stop sending work when the service is unbound so until * then we keep the wakelock. - * @param name The concrete component name of the service that has - * been connected. + * @param name The concrete component name of the service that has been connected. * @param service The IBinder of the Service's communication channel, */ @Override public void onServiceConnected(ComponentName name, IBinder service) { - mBound = true; + if (!name.equals(mRunningTask.getServiceComponent())) { + mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); + return; + } this.service = ITaskService.Stub.asInterface(service); - // Remove all timeouts. We've just connected to the client so there are no other - // MSG_TIMEOUTs at this point. + // Remove all timeouts. mCallbackHandler.removeMessages(MSG_TIMEOUT); mWakeLock.acquire(); - mCallbackHandler.obtainMessage(MSG_CHECK_PENDING).sendToTarget(); + mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget(); } /** - * When the client service crashes we can have a couple tasks executing, in various stages of - * undress. We'll cancel all of them and request that they be rescheduled. + * If the client service crashes we reschedule this task and clean up. * @param name The concrete component name of the service whose */ @Override public void onServiceDisconnected(ComponentName name) { - // Service disconnected... probably client crashed. - startShutdown(); + mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); } /** - * We don't just shutdown outright - we make sure the scheduler isn't going to send us any more - * tasks, then we do the shutdown. + * This class is reused across different clients, and passes itself in as a callback. Check + * whether the client exercising the callback is the client we expect. + * @return True if the binder calling is coming from the client we expect. */ - private void startShutdown() { - mCompletedListener.onAllTasksCompleted(token); - mCallbackHandler.obtainMessage(MSG_SHUTDOWN).sendToTarget(); - } - - /** Tracks a task across its various state changes. */ - private static class ActiveTask { - final TaskParams params; - int verb; - AtomicBoolean cancelled = new AtomicBoolean(); - - ActiveTask(TaskParams params, int verb) { - this.params = params; - this.verb = verb; - } - - @Override - public String toString() { - return params.getTaskId() + " " + VERB_STRINGS[verb]; + private boolean verifyCallingUid() { + if (mRunningTask == null || Binder.getCallingUid() != mRunningTask.getUid()) { + if (DEBUG) { + Slog.d(TAG, "Stale callback received, ignoring."); + } + return false; } + return true; } /** @@ -264,52 +258,67 @@ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceCon @Override public void handleMessage(Message message) { switch (message.what) { - case MSG_ADD_PENDING: - if (message.obj != null) { - ActiveTask pendingTask = (ActiveTask) message.obj; - mPending.put(pendingTask.params.getTaskId(), pendingTask); - } - // fall through. - case MSG_CHECK_PENDING: - checkPendingTasksH(); + case MSG_SERVICE_BOUND: + handleServiceBoundH(); break; case MSG_CALLBACK: - ActiveTask receivedCallback = mPending.get(message.arg1); - removeMessages(MSG_TIMEOUT, receivedCallback); - - if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) { - Log.d(TAG, "MSG_CALLBACK of : " + receivedCallback); + if (DEBUG) { + Slog.d(TAG, "MSG_CALLBACK of : " + mRunningTask); } + removeMessages(MSG_TIMEOUT); - if (receivedCallback.verb == VERB_STARTING) { + if (mVerb == VERB_STARTING) { final boolean workOngoing = message.arg2 == 1; - handleStartedH(receivedCallback, workOngoing); - } else if (receivedCallback.verb == VERB_EXECUTING || - receivedCallback.verb == VERB_STOPPING) { + handleStartedH(workOngoing); + } else if (mVerb == VERB_EXECUTING || + mVerb == VERB_STOPPING) { final boolean reschedule = message.arg2 == 1; - handleFinishedH(receivedCallback, reschedule); + handleFinishedH(reschedule); } else { - if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) { - Log.d(TAG, "Unrecognised callback: " + receivedCallback); + if (DEBUG) { + Slog.d(TAG, "Unrecognised callback: " + mRunningTask); } } break; case MSG_CANCEL: - ActiveTask cancelled = mPending.get(message.arg1); - handleCancelH(cancelled); + handleCancelH(); break; case MSG_TIMEOUT: - // Timeout msgs have the ActiveTask ref so we can remove them easily. - handleOpTimeoutH((ActiveTask) message.obj); - break; - case MSG_SHUTDOWN: - handleShutdownH(); + handleOpTimeoutH(); break; + case MSG_SHUTDOWN_EXECUTION: + closeAndCleanupTaskH(true /* needsReschedule */); default: Log.e(TAG, "Unrecognised message: " + message); } } + /** Start the task on the service. */ + private void handleServiceBoundH() { + if (mVerb != VERB_BINDING) { + Slog.e(TAG, "Sending onStartTask for a task that isn't pending. " + + VERB_STRINGS[mVerb]); + closeAndCleanupTaskH(false /* reschedule */); + return; + } + if (mCancelled.get()) { + if (DEBUG) { + Slog.d(TAG, "Task cancelled while waiting for bind to complete. " + + mRunningTask); + } + closeAndCleanupTaskH(true /* reschedule */); + return; + } + try { + mVerb = VERB_STARTING; + scheduleOpTimeOut(); + service.startTask(mParams); + } catch (RemoteException e) { + Log.e(TAG, "Error sending onStart message to '" + + mRunningTask.getServiceComponent().getShortClassName() + "' ", e); + } + } + /** * State behaviours. * VERB_STARTING -> Successful start, change task to VERB_EXECUTING and post timeout. @@ -317,24 +326,25 @@ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceCon * _EXECUTING -> Error * _STOPPING -> Error */ - private void handleStartedH(ActiveTask started, boolean workOngoing) { - switch (started.verb) { + private void handleStartedH(boolean workOngoing) { + switch (mVerb) { case VERB_STARTING: - started.verb = VERB_EXECUTING; + mVerb = VERB_EXECUTING; if (!workOngoing) { // Task is finished already so fast-forward to handleFinished. - handleFinishedH(started, false); + handleFinishedH(false); return; - } else if (started.cancelled.get()) { + } + if (mCancelled.get()) { // Cancelled *while* waiting for acknowledgeStartMessage from client. - handleCancelH(started); + handleCancelH(); return; - } else { - scheduleOpTimeOut(started); } + scheduleOpTimeOut(); break; default: - Log.e(TAG, "Handling started task but task wasn't starting! " + started); + Log.e(TAG, "Handling started task but task wasn't starting! Was " + + VERB_STRINGS[mVerb] + "."); return; } } @@ -345,155 +355,104 @@ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceCon * _STARTING -> Error * _PENDING -> Error */ - private void handleFinishedH(ActiveTask executedTask, boolean reschedule) { - switch (executedTask.verb) { + private void handleFinishedH(boolean reschedule) { + switch (mVerb) { case VERB_EXECUTING: case VERB_STOPPING: - closeAndCleanupTaskH(executedTask, reschedule); + closeAndCleanupTaskH(reschedule); break; default: - Log.e(TAG, "Got an execution complete message for a task that wasn't being" + - "executed. " + executedTask); + Slog.e(TAG, "Got an execution complete message for a task that wasn't being" + + "executed. Was " + VERB_STRINGS[mVerb] + "."); } } /** * A task can be in various states when a cancel request comes in: - * VERB_PENDING -> Remove from queue. - * _STARTING -> Mark as cancelled and wait for {@link #acknowledgeStartMessage(int)}. + * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for + * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} + * _STARTING -> Mark as cancelled and wait for + * {@link TaskServiceContext#acknowledgeStartMessage(int, boolean)} * _EXECUTING -> call {@link #sendStopMessageH}}. * _ENDING -> No point in doing anything here, so we ignore. */ - private void handleCancelH(ActiveTask cancelledTask) { - switch (cancelledTask.verb) { - case VERB_PENDING: - mPending.remove(cancelledTask.params.getTaskId()); - break; + private void handleCancelH() { + switch (mVerb) { + case VERB_BINDING: case VERB_STARTING: - cancelledTask.cancelled.set(true); + mCancelled.set(true); break; case VERB_EXECUTING: - cancelledTask.verb = VERB_STOPPING; - sendStopMessageH(cancelledTask); + sendStopMessageH(); break; case VERB_STOPPING: // Nada. break; default: - Log.e(TAG, "Cancelling a task without a valid verb: " + cancelledTask); + Slog.e(TAG, "Cancelling a task without a valid verb: " + mVerb); break; } } - /** - * This TaskServiceContext is shutting down. Remove all the tasks from the pending queue - * and reschedule them as if they had failed. - * Before posting this message, caller must invoke - * {@link com.android.server.task.TaskCompletedListener#onAllTasksCompleted(int)}. - */ - private void handleShutdownH() { - for (int i = 0; i < mPending.size(); i++) { - ActiveTask at = mPending.valueAt(i); - closeAndCleanupTaskH(at, true /* needsReschedule */); - } - mWakeLock.release(); - mContext.unbindService(TaskServiceContext.this); - service = null; - mBound = false; - } - - /** - * MSG_TIMEOUT gets processed here. - * @param timedOutTask The task that timed out. - */ - private void handleOpTimeoutH(ActiveTask timedOutTask) { + /** Process MSG_TIMEOUT here. */ + private void handleOpTimeoutH() { if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) { - Log.d(TAG, "MSG_TIMEOUT of " + component.getShortClassName() + " : " - + timedOutTask.params.getTaskId()); + Log.d(TAG, "MSG_TIMEOUT of " + + mRunningTask.getServiceComponent().getShortClassName() + " : " + + mParams.getTaskId()); } - final int taskId = timedOutTask.params.getTaskId(); - switch (timedOutTask.verb) { + final int taskId = mParams.getTaskId(); + switch (mVerb) { case VERB_STARTING: // Client unresponsive - wedged or failed to respond in time. We don't really // know what happened so let's log it and notify the TaskManager // FINISHED/NO-RETRY. Log.e(TAG, "No response from client for onStartTask '" + - component.getShortClassName() + "' tId: " + taskId); - closeAndCleanupTaskH(timedOutTask, false /* needsReschedule */); + mRunningTask.getServiceComponent().getShortClassName() + "' tId: " + + taskId); + closeAndCleanupTaskH(false /* needsReschedule */); break; case VERB_STOPPING: // At least we got somewhere, so fail but ask the TaskManager to reschedule. Log.e(TAG, "No response from client for onStopTask, '" + - component.getShortClassName() + "' tId: " + taskId); - closeAndCleanupTaskH(timedOutTask, true /* needsReschedule */); + mRunningTask.getServiceComponent().getShortClassName() + "' tId: " + + taskId); + closeAndCleanupTaskH(true /* needsReschedule */); break; case VERB_EXECUTING: // Not an error - client ran out of time. Log.i(TAG, "Client timed out while executing (no taskFinished received)." + " Reporting failure and asking for reschedule. " + - component.getShortClassName() + "' tId: " + taskId); - sendStopMessageH(timedOutTask); + mRunningTask.getServiceComponent().getShortClassName() + "' tId: " + + taskId); + sendStopMessageH(); break; default: Log.e(TAG, "Handling timeout for an unknown active task state: " - + timedOutTask); + + mRunningTask); return; } } /** - * Called on the handler thread. Checks the state of the pending queue and starts the task - * if it can. The task only starts if there is capacity on the service. - */ - private void checkPendingTasksH() { - if (!mBound) { - return; - } - for (int i = 0; i < mPending.size() && i < defaultMaxActiveTasksPerService; i++) { - ActiveTask at = mPending.valueAt(i); - if (at.verb != VERB_PENDING) { - continue; - } - sendStartMessageH(at); - } - } - - /** - * Already running, need to stop. Rund on handler. - * @param stoppingTask Task we are sending onStopMessage for. This task will be moved from - * VERB_EXECUTING -> VERB_STOPPING. + * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> + * VERB_STOPPING. */ - private void sendStopMessageH(ActiveTask stoppingTask) { - mCallbackHandler.removeMessages(MSG_TIMEOUT, stoppingTask); - if (stoppingTask.verb != VERB_EXECUTING) { - Log.e(TAG, "Sending onStopTask for a task that isn't started. " + stoppingTask); - // TODO: Handle error? + private void sendStopMessageH() { + mCallbackHandler.removeMessages(MSG_TIMEOUT); + if (mVerb != VERB_EXECUTING) { + Log.e(TAG, "Sending onStopTask for a task that isn't started. " + mRunningTask); + closeAndCleanupTaskH(false /* reschedule */); return; } try { - service.stopTask(stoppingTask.params); - stoppingTask.verb = VERB_STOPPING; - scheduleOpTimeOut(stoppingTask); + mVerb = VERB_STOPPING; + scheduleOpTimeOut(); + service.stopTask(mParams); } catch (RemoteException e) { Log.e(TAG, "Error sending onStopTask to client.", e); - closeAndCleanupTaskH(stoppingTask, false); - } - } - - /** Start the task on the service. */ - private void sendStartMessageH(ActiveTask pendingTask) { - if (pendingTask.verb != VERB_PENDING) { - Log.e(TAG, "Sending onStartTask for a task that isn't pending. " + pendingTask); - // TODO: Handle error? - } - try { - service.startTask(pendingTask.params); - pendingTask.verb = VERB_STARTING; - scheduleOpTimeOut(pendingTask); - } catch (RemoteException e) { - Log.e(TAG, "Error sending onStart message to '" + component.getShortClassName() - + "' ", e); + closeAndCleanupTaskH(false); } } @@ -503,13 +462,42 @@ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceCon * or from acknowledging the stop message we sent. Either way, we're done tracking it and * we want to clean up internally. */ - private void closeAndCleanupTaskH(ActiveTask completedTask, boolean reschedule) { - removeMessages(MSG_TIMEOUT, completedTask); - mPending.remove(completedTask.params.getTaskId()); - if (mPending.size() == 0) { - startShutdown(); + private void closeAndCleanupTaskH(boolean reschedule) { + removeMessages(MSG_TIMEOUT); + mWakeLock.release(); + mContext.unbindService(TaskServiceContext.this); + mWakeLock = null; + + mRunningTask = null; + mParams = null; + mVerb = -1; + mCancelled.set(false); + + service = null; + + mCompletedListener.onTaskCompleted(mRunningTask, reschedule); + synchronized (mAvailableLock) { + mAvailable = true; + } + } + + /** + * Called when sending a message to the client, over whose execution we have no control. If we + * haven't received a response in a certain amount of time, we want to give up and carry on + * with life. + */ + private void scheduleOpTimeOut() { + mCallbackHandler.removeMessages(MSG_TIMEOUT); + + final long timeoutMillis = (mVerb == VERB_EXECUTING) ? + EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS; + if (DEBUG) { + Slog.d(TAG, "Scheduling time out for '" + + mRunningTask.getServiceComponent().getShortClassName() + "' tId: " + + mParams.getTaskId() + ", in " + (timeoutMillis / 1000) + " s"); } - mCompletedListener.onTaskCompleted(token, completedTask.params.getTaskId(), reschedule); + Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT); + mCallbackHandler.sendMessageDelayed(m, timeoutMillis); } } } diff --git a/services/core/java/com/android/server/task/TaskStore.java b/services/core/java/com/android/server/task/TaskStore.java index 81187c8..f72ab22 100644 --- a/services/core/java/com/android/server/task/TaskStore.java +++ b/services/core/java/com/android/server/task/TaskStore.java @@ -18,10 +18,16 @@ package com.android.server.task; import android.app.task.Task; import android.content.Context; +import android.util.ArraySet; +import android.util.Slog; import android.util.SparseArray; import com.android.server.task.controllers.TaskStatus; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + /** * Maintain a list of classes, and accessor methods/logic for these tasks. * This class offers the following functionality: @@ -35,76 +41,131 @@ import com.android.server.task.controllers.TaskStatus; * - This class is <strong>not</strong> thread-safe. */ public class TaskStore { - - /** - * Master list, indexed by {@link com.android.server.task.controllers.TaskStatus#hashCode()}. - */ - final SparseArray<TaskStatus> mTasks; + private static final String TAG = "TaskManagerStore"; + /** Threshold to adjust how often we want to write to the db. */ + private static final int MAX_OPS_BEFORE_WRITE = 1; + final ArraySet<TaskStatus> mTasks; final Context mContext; + private int mDirtyOperations; + TaskStore(Context context) { - mTasks = intialiseTaskMapFromDisk(); + mTasks = intialiseTasksFromDisk(); mContext = context; + mDirtyOperations = 0; } /** - * Add a task to the master list, persisting it if necessary. - * Will first check to see if the task already exists. If so, it will replace it. - * {@link android.content.pm.PackageManager} is queried to see if the calling package has - * permission to - * @param task Task to add. - * @return The initialised TaskStatus object if this operation was successful, null if it - * failed. + * Add a task to the master list, persisting it if necessary. If the TaskStatus already exists, + * it will be replaced. + * @param taskStatus Task to add. + * @return true if the operation succeeded. */ - public TaskStatus addNewTaskForUser(Task task, int userId, int uId, - boolean canPersistTask) { - TaskStatus taskStatus = TaskStatus.getForTaskAndUser(task, userId, uId); - if (canPersistTask && task.isPeriodic()) { - if (writeStatusToDisk()) { - mTasks.put(taskStatus.hashCode(), taskStatus); + public boolean add(TaskStatus taskStatus) { + if (taskStatus.isPersisted()) { + if (!maybeWriteStatusToDisk()) { + return false; } } - return taskStatus; + mTasks.remove(taskStatus); + mTasks.add(taskStatus); + return true; } - /** - * Remove the provided task. Will also delete the task if it was persisted. Note that this - * function does not return the validity of the operation, as we assume a delete will always - * succeed. - * @param task Task to remove. - */ - public void remove(Task task) { - + public int size() { + return mTasks.size(); } /** - * Every time the state changes we write all the tasks in one swathe, instead of trying to - * track incremental changes. + * Remove the provided task. Will also delete the task if it was persisted. + * @return The TaskStatus that was removed, or null if an invalid token was provided. */ - private boolean writeStatusToDisk() { + public boolean remove(TaskStatus taskStatus) { + boolean removed = mTasks.remove(taskStatus); + if (!removed) { + Slog.e(TAG, "Error removing task: " + taskStatus); + return false; + } else { + maybeWriteStatusToDisk(); + } return true; } /** - * - * @return + * Removes all TaskStatus objects for a given uid from the master list. Note that it is + * possible to remove a task that is pending/active. This operation will succeed, and the + * removal will take effect when the task has completed executing. + * @param uid Uid of the requesting app. + * @return True if at least one task was removed, false if nothing matching the provided uId + * was found. */ - // TODO: Implement this. - private SparseArray<TaskStatus> intialiseTaskMapFromDisk() { - return new SparseArray<TaskStatus>(); + public boolean removeAllByUid(int uid) { + Iterator<TaskStatus> it = mTasks.iterator(); + boolean removed = false; + while (it.hasNext()) { + TaskStatus ts = it.next(); + if (ts.getUid() == uid) { + it.remove(); + removed = true; + } + } + if (removed) { + maybeWriteStatusToDisk(); + } + return removed; } /** - * @return the number of tasks in the store + * Remove the TaskStatus that matches the provided uId and taskId. Note that it is possible + * to remove a task that is pending/active. This operation will succeed, and the removal will + * take effect when the task has completed executing. + * @param uid Uid of the requesting app. + * @param taskId Task id, specified at schedule-time. + * @return true if a removal occurred, false if the provided parameters didn't match anything. */ - public int size() { - return mTasks.size(); + public boolean remove(int uid, int taskId) { + Iterator<TaskStatus> it = mTasks.iterator(); + while (it.hasNext()) { + TaskStatus ts = it.next(); + if (ts.getUid() == uid && ts.getTaskId() == taskId) { + it.remove(); + maybeWriteStatusToDisk(); + return true; + } + } + return false; } /** * @return The live array of TaskStatus objects. */ - public SparseArray<TaskStatus> getTasks() { + public Set<TaskStatus> getTasks() { return mTasks; } + + /** + * Every time the state changes we write all the tasks in one swathe, instead of trying to + * track incremental changes. + * @return Whether the operation was successful. This will only fail for e.g. if the system is + * low on storage. If this happens, we continue as normal + */ + private boolean maybeWriteStatusToDisk() { + mDirtyOperations++; + if (mDirtyOperations > MAX_OPS_BEFORE_WRITE) { + for (TaskStatus ts : mTasks) { + // + } + mDirtyOperations = 0; + } + return true; + } + + /** + * + * @return + */ + // TODO: Implement this. + private ArraySet<TaskStatus> intialiseTasksFromDisk() { + return new ArraySet<TaskStatus>(); + } } diff --git a/services/core/java/com/android/server/task/controllers/ConnectivityController.java b/services/core/java/com/android/server/task/controllers/ConnectivityController.java index a0038c5..474af8f 100644 --- a/services/core/java/com/android/server/task/controllers/ConnectivityController.java +++ b/services/core/java/com/android/server/task/controllers/ConnectivityController.java @@ -25,6 +25,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.UserHandle; import android.util.Log; +import android.util.Slog; import com.android.server.task.TaskManagerService; @@ -32,21 +33,33 @@ import java.util.LinkedList; import java.util.List; /** - * + * Handles changes in connectivity. + * We are only interested in metered vs. unmetered networks, and we're interested in them on a + * per-user basis. */ public class ConnectivityController extends StateController { private static final String TAG = "TaskManager.Connectivity"; + private static final boolean DEBUG = true; private final List<TaskStatus> mTrackedTasks = new LinkedList<TaskStatus>(); private final BroadcastReceiver mConnectivityChangedReceiver = new ConnectivityChangedReceiver(); + /** Singleton. */ + private static ConnectivityController mSingleton; /** Track whether the latest active network is metered. */ private boolean mMetered; /** Track whether the latest active network is connected. */ private boolean mConnectivity; - public ConnectivityController(TaskManagerService service) { + public static synchronized ConnectivityController get(TaskManagerService taskManager) { + if (mSingleton == null) { + mSingleton = new ConnectivityController(taskManager); + } + return mSingleton; + } + + private ConnectivityController(TaskManagerService service) { super(service); // Register connectivity changed BR. IntentFilter intentFilter = new IntentFilter(); @@ -56,7 +69,7 @@ public class ConnectivityController extends StateController { } @Override - public void maybeTrackTaskState(TaskStatus taskStatus) { + public synchronized void maybeStartTrackingTask(TaskStatus taskStatus) { if (taskStatus.hasConnectivityConstraint() || taskStatus.hasMeteredConstraint()) { taskStatus.connectivityConstraintSatisfied.set(mConnectivity); taskStatus.meteredConstraintSatisfied.set(mMetered); @@ -65,21 +78,28 @@ public class ConnectivityController extends StateController { } @Override - public void removeTaskStateIfTracked(TaskStatus taskStatus) { + public synchronized void maybeStopTrackingTask(TaskStatus taskStatus) { mTrackedTasks.remove(taskStatus); } /** - * + * @param userId Id of the user for whom we are updating the connectivity state. */ - private void updateTrackedTasks() { + private void updateTrackedTasks(int userId) { + boolean changed = false; for (TaskStatus ts : mTrackedTasks) { + if (ts.getUserId() != userId) { + continue; + } boolean prevIsConnected = ts.connectivityConstraintSatisfied.getAndSet(mConnectivity); boolean prevIsMetered = ts.meteredConstraintSatisfied.getAndSet(mMetered); if (prevIsConnected != mConnectivity || prevIsMetered != mMetered) { - mStateChangedListener.onTaskStateChanged(ts); + changed = true; } } + if (changed) { + mStateChangedListener.onControllerStateChanged(); + } } class ConnectivityChangedReceiver extends BroadcastReceiver { @@ -103,17 +123,20 @@ public class ConnectivityController extends StateController { context.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo activeNetwork = connManager.getActiveNetworkInfo(); // This broadcast gets sent a lot, only update if the active network has changed. - if (activeNetwork.getType() == networkType) { + if (activeNetwork != null && activeNetwork.getType() == networkType) { + final int userid = context.getUserId(); mMetered = false; mConnectivity = !intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); if (mConnectivity) { // No point making the call if we know there's no conn. mMetered = connManager.isActiveNetworkMetered(); } - updateTrackedTasks(); + updateTrackedTasks(userid); } } else { - Log.w(TAG, "Unrecognised action in intent: " + action); + if (DEBUG) { + Slog.d(TAG, "Unrecognised action in intent: " + action); + } } } }; diff --git a/services/core/java/com/android/server/task/controllers/IdleController.java b/services/core/java/com/android/server/task/controllers/IdleController.java index a319a31..9489644 100644 --- a/services/core/java/com/android/server/task/controllers/IdleController.java +++ b/services/core/java/com/android/server/task/controllers/IdleController.java @@ -49,7 +49,7 @@ public class IdleController extends StateController { private static Object sCreationLock = new Object(); private static volatile IdleController sController; - public IdleController getController(TaskManagerService service) { + public static IdleController get(TaskManagerService service) { synchronized (sCreationLock) { if (sController == null) { sController = new IdleController(service); @@ -67,7 +67,7 @@ public class IdleController extends StateController { * StateController interface */ @Override - public void maybeTrackTaskState(TaskStatus taskStatus) { + public void maybeStartTrackingTask(TaskStatus taskStatus) { if (taskStatus.hasIdleConstraint()) { synchronized (mTrackedTasks) { mTrackedTasks.add(taskStatus); @@ -77,7 +77,7 @@ public class IdleController extends StateController { } @Override - public void removeTaskStateIfTracked(TaskStatus taskStatus) { + public void maybeStopTrackingTask(TaskStatus taskStatus) { synchronized (mTrackedTasks) { mTrackedTasks.remove(taskStatus); } @@ -90,9 +90,9 @@ public class IdleController extends StateController { synchronized (mTrackedTasks) { for (TaskStatus task : mTrackedTasks) { task.idleConstraintSatisfied.set(isIdle); - mStateChangedListener.onTaskStateChanged(task); } } + mStateChangedListener.onControllerStateChanged(); } /** diff --git a/services/core/java/com/android/server/task/controllers/StateController.java b/services/core/java/com/android/server/task/controllers/StateController.java index e1cd662..ed31eac 100644 --- a/services/core/java/com/android/server/task/controllers/StateController.java +++ b/services/core/java/com/android/server/task/controllers/StateController.java @@ -42,10 +42,10 @@ public abstract class StateController { * Also called when updating a task, so implementing controllers have to be aware of * preexisting tasks. */ - public abstract void maybeTrackTaskState(TaskStatus taskStatus); + public abstract void maybeStartTrackingTask(TaskStatus taskStatus); /** * Remove task - this will happen if the task is cancelled, completed, etc. */ - public abstract void removeTaskStateIfTracked(TaskStatus taskStatus); + public abstract void maybeStopTrackingTask(TaskStatus taskStatus); } diff --git a/services/core/java/com/android/server/task/controllers/TaskStatus.java b/services/core/java/com/android/server/task/controllers/TaskStatus.java index d270016..b7f84ec 100644 --- a/services/core/java/com/android/server/task/controllers/TaskStatus.java +++ b/services/core/java/com/android/server/task/controllers/TaskStatus.java @@ -18,7 +18,6 @@ package com.android.server.task.controllers; import android.app.task.Task; import android.content.ComponentName; -import android.content.pm.PackageParser; import android.os.Bundle; import android.os.SystemClock; import android.os.UserHandle; @@ -39,65 +38,67 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public class TaskStatus { final Task task; - final int taskId; final int uId; - final Bundle extras; + /** At reschedule time we need to know whether to update task on disk. */ + final boolean persisted; + + // Constraints. final AtomicBoolean chargingConstraintSatisfied = new AtomicBoolean(); - final AtomicBoolean timeConstraintSatisfied = new AtomicBoolean(); + final AtomicBoolean timeDelayConstraintSatisfied = new AtomicBoolean(); + final AtomicBoolean deadlineConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean idleConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean meteredConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean connectivityConstraintSatisfied = new AtomicBoolean(); - private final boolean hasChargingConstraint; - private final boolean hasTimingConstraint; - private final boolean hasIdleConstraint; - private final boolean hasMeteredConstraint; - private final boolean hasConnectivityConstraint; - + /** + * Earliest point in the future at which this task will be eligible to run. A value of 0 + * indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}. + */ private long earliestRunTimeElapsedMillis; + /** + * Latest point in the future at which this task must be run. A value of {@link Long#MAX_VALUE} + * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}. + */ private long latestRunTimeElapsedMillis; + private final int numFailures; + /** Provide a handle to the service that this task will be run on. */ public int getServiceToken() { return uId; } - /** Generate a TaskStatus object for a given task and uid. */ - // TODO: reimplement this to reuse these objects instead of creating a new one each time? - public static TaskStatus getForTaskAndUser(Task task, int userId, int uId) { - return new TaskStatus(task, userId, uId); - } - - /** Set up the state of a newly scheduled task. */ - TaskStatus(Task task, int userId, int uId) { + /** Create a newly scheduled task. */ + public TaskStatus(Task task, int uId, boolean persisted) { this.task = task; - this.taskId = task.getTaskId(); - this.extras = task.getExtras(); this.uId = uId; + this.numFailures = 0; + this.persisted = persisted; - hasChargingConstraint = task.isRequireCharging(); - hasIdleConstraint = task.isRequireDeviceIdle(); - + final long elapsedNow = SystemClock.elapsedRealtime(); // Timing constraints if (task.isPeriodic()) { - long elapsedNow = SystemClock.elapsedRealtime(); earliestRunTimeElapsedMillis = elapsedNow; latestRunTimeElapsedMillis = elapsedNow + task.getIntervalMillis(); - hasTimingConstraint = true; - } else if (task.getMinLatencyMillis() != 0L || task.getMaxExecutionDelayMillis() != 0L) { - earliestRunTimeElapsedMillis = task.getMinLatencyMillis() > 0L ? - task.getMinLatencyMillis() : Long.MAX_VALUE; - latestRunTimeElapsedMillis = task.getMaxExecutionDelayMillis() > 0L ? - task.getMaxExecutionDelayMillis() : Long.MAX_VALUE; - hasTimingConstraint = true; } else { - hasTimingConstraint = false; + earliestRunTimeElapsedMillis = task.hasEarlyConstraint() ? + elapsedNow + task.getMinLatencyMillis() : 0L; + latestRunTimeElapsedMillis = task.hasLateConstraint() ? + elapsedNow + task.getMaxExecutionDelayMillis() : Long.MAX_VALUE; } + } + + public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsed, + long newLatestRuntimeElapsed, int backoffAttempt) { + this.task = rescheduling.task; - // Networking constraints - hasMeteredConstraint = task.getNetworkCapabilities() == Task.NetworkType.UNMETERED; - hasConnectivityConstraint = task.getNetworkCapabilities() == Task.NetworkType.ANY; + this.uId = rescheduling.getUid(); + this.persisted = rescheduling.isPersisted(); + this.numFailures = backoffAttempt; + + earliestRunTimeElapsedMillis = newEarliestRuntimeElapsed; + latestRunTimeElapsedMillis = newLatestRuntimeElapsed; } public Task getTask() { @@ -105,7 +106,11 @@ public class TaskStatus { } public int getTaskId() { - return taskId; + return task.getId(); + } + + public int getNumFailures() { + return numFailures; } public ComponentName getServiceComponent() { @@ -121,52 +126,60 @@ public class TaskStatus { } public Bundle getExtras() { - return extras; + return task.getExtras(); + } + + public boolean hasConnectivityConstraint() { + return task.getNetworkCapabilities() == Task.NetworkType.ANY; } - boolean hasConnectivityConstraint() { - return hasConnectivityConstraint; + public boolean hasMeteredConstraint() { + return task.getNetworkCapabilities() == Task.NetworkType.UNMETERED; } - boolean hasMeteredConstraint() { - return hasMeteredConstraint; + public boolean hasChargingConstraint() { + return task.isRequireCharging(); } - boolean hasChargingConstraint() { - return hasChargingConstraint; + public boolean hasTimingDelayConstraint() { + return earliestRunTimeElapsedMillis != 0L; } - boolean hasTimingConstraint() { - return hasTimingConstraint; + public boolean hasDeadlineConstraint() { + return latestRunTimeElapsedMillis != Long.MAX_VALUE; } - boolean hasIdleConstraint() { - return hasIdleConstraint; + public boolean hasIdleConstraint() { + return task.isRequireDeviceIdle(); } - long getEarliestRunTime() { + public long getEarliestRunTime() { return earliestRunTimeElapsedMillis; } - long getLatestRunTime() { + public long getLatestRunTimeElapsed() { return latestRunTimeElapsedMillis; } + public boolean isPersisted() { + return persisted; + } /** - * @return whether this task is ready to run, based on its requirements. + * @return Whether or not this task is ready to run, based on its requirements. */ public synchronized boolean isReady() { - return (!hasChargingConstraint || chargingConstraintSatisfied.get()) - && (!hasTimingConstraint || timeConstraintSatisfied.get()) - && (!hasConnectivityConstraint || connectivityConstraintSatisfied.get()) - && (!hasMeteredConstraint || meteredConstraintSatisfied.get()) - && (!hasIdleConstraint || idleConstraintSatisfied.get()); + return (!hasChargingConstraint() || chargingConstraintSatisfied.get()) + && (!hasTimingDelayConstraint() || timeDelayConstraintSatisfied.get()) + && (!hasConnectivityConstraint() || connectivityConstraintSatisfied.get()) + && (!hasMeteredConstraint() || meteredConstraintSatisfied.get()) + && (!hasIdleConstraint() || idleConstraintSatisfied.get()) + && (!hasDeadlineConstraint() || deadlineConstraintSatisfied.get()); } @Override public int hashCode() { int result = getServiceComponent().hashCode(); - result = 31 * result + taskId; + result = 31 * result + task.getId(); result = 31 * result + uId; return result; } @@ -177,14 +190,14 @@ public class TaskStatus { if (!(o instanceof TaskStatus)) return false; TaskStatus that = (TaskStatus) o; - return ((taskId == that.taskId) + return ((task.getId() == that.task.getId()) && (uId == that.uId) && (getServiceComponent().equals(that.getServiceComponent()))); } // Dumpsys infrastructure public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.print("Task "); pw.println(taskId); + pw.print(prefix); pw.print("Task "); pw.println(task.getId()); pw.print(prefix); pw.print("uid="); pw.println(uId); pw.print(prefix); pw.print("component="); pw.println(task.getService()); } diff --git a/services/core/java/com/android/server/task/controllers/TimeController.java b/services/core/java/com/android/server/task/controllers/TimeController.java index 6d97a53..72f312c 100644 --- a/services/core/java/com/android/server/task/controllers/TimeController.java +++ b/services/core/java/com/android/server/task/controllers/TimeController.java @@ -23,7 +23,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.SystemClock; -import android.util.Log; import com.android.server.task.TaskManagerService; @@ -54,8 +53,17 @@ public class TimeController extends StateController { private AlarmManager mAlarmService = null; /** List of tracked tasks, sorted asc. by deadline */ private final List<TaskStatus> mTrackedTasks = new LinkedList<TaskStatus>(); + /** Singleton. */ + private static TimeController mSingleton; - public TimeController(TaskManagerService service) { + public static synchronized TimeController get(TaskManagerService taskManager) { + if (mSingleton == null) { + mSingleton = new TimeController(taskManager); + } + return mSingleton; + } + + private TimeController(TaskManagerService service) { super(service); mTaskExpiredAlarmIntent = PendingIntent.getBroadcast(mContext, 0 /* ignored */, @@ -75,8 +83,8 @@ public class TimeController extends StateController { * list. */ @Override - public synchronized void maybeTrackTaskState(TaskStatus task) { - if (task.hasTimingConstraint()) { + public synchronized void maybeStartTrackingTask(TaskStatus task) { + if (task.hasTimingDelayConstraint()) { ListIterator<TaskStatus> it = mTrackedTasks.listIterator(mTrackedTasks.size()); while (it.hasPrevious()) { TaskStatus ts = it.previous(); @@ -85,13 +93,13 @@ public class TimeController extends StateController { it.remove(); it.add(task); break; - } else if (ts.getLatestRunTime() < task.getLatestRunTime()) { + } else if (ts.getLatestRunTimeElapsed() < task.getLatestRunTimeElapsed()) { // Insert it.add(task); break; } } - maybeUpdateAlarms(task.getEarliestRunTime(), task.getLatestRunTime()); + maybeUpdateAlarms(task.getEarliestRunTime(), task.getLatestRunTimeElapsed()); } } @@ -100,12 +108,12 @@ public class TimeController extends StateController { * so, update them. */ @Override - public synchronized void removeTaskStateIfTracked(TaskStatus taskStatus) { + public synchronized void maybeStopTrackingTask(TaskStatus taskStatus) { if (mTrackedTasks.remove(taskStatus)) { if (mNextDelayExpiredElapsedMillis <= taskStatus.getEarliestRunTime()) { handleTaskDelayExpired(); } - if (mNextTaskExpiredElapsedMillis <= taskStatus.getLatestRunTime()) { + if (mNextTaskExpiredElapsedMillis <= taskStatus.getLatestRunTimeElapsed()) { handleTaskDeadlineExpired(); } } @@ -140,10 +148,10 @@ public class TimeController extends StateController { * back and forth. */ private boolean canStopTrackingTask(TaskStatus taskStatus) { - final long elapsedNowMillis = SystemClock.elapsedRealtime(); - return taskStatus.timeConstraintSatisfied.get() && - (taskStatus.getLatestRunTime() == Long.MAX_VALUE || - taskStatus.getLatestRunTime() < elapsedNowMillis); + return (!taskStatus.hasTimingDelayConstraint() || + taskStatus.timeDelayConstraintSatisfied.get()) && + (!taskStatus.hasDeadlineConstraint() || + taskStatus.deadlineConstraintSatisfied.get()); } private void maybeUpdateAlarms(long delayExpiredElapsed, long deadlineExpiredElapsed) { @@ -174,10 +182,10 @@ public class TimeController extends StateController { Iterator<TaskStatus> it = mTrackedTasks.iterator(); while (it.hasNext()) { TaskStatus ts = it.next(); - final long taskDeadline = ts.getLatestRunTime(); + final long taskDeadline = ts.getLatestRunTimeElapsed(); if (taskDeadline <= nowElapsedMillis) { - ts.timeConstraintSatisfied.set(true); + ts.deadlineConstraintSatisfied.set(true); mStateChangedListener.onTaskDeadlineExpired(ts); it.remove(); } else { // Sorted by expiry time, so take the next one and stop. @@ -199,10 +207,12 @@ public class TimeController extends StateController { Iterator<TaskStatus> it = mTrackedTasks.iterator(); while (it.hasNext()) { final TaskStatus ts = it.next(); + if (!ts.hasTimingDelayConstraint()) { + continue; + } final long taskDelayTime = ts.getEarliestRunTime(); if (taskDelayTime < nowElapsedMillis) { - ts.timeConstraintSatisfied.set(true); - mStateChangedListener.onTaskStateChanged(ts); + ts.timeDelayConstraintSatisfied.set(true); if (canStopTrackingTask(ts)) { it.remove(); } @@ -212,6 +222,7 @@ public class TimeController extends StateController { } } } + mStateChangedListener.onControllerStateChanged(); maybeUpdateAlarms(nextDelayTime, Long.MAX_VALUE); } |