diff options
author | Matthew Williams <mjwilliams@google.com> | 2014-05-12 15:33:09 -0700 |
---|---|---|
committer | Matthew Williams <mjwilliams@google.com> | 2014-05-16 10:33:48 -0700 |
commit | 691e93e82cf97338a760c5fbf900ef29ed9224c2 (patch) | |
tree | 44c31eb7e10aeca075c5b4a6f4969299cf95d5bf /services | |
parent | 8974f2490992b2921b636c0b65ebeb5b19dda89b (diff) | |
download | frameworks_base-691e93e82cf97338a760c5fbf900ef29ed9224c2.zip frameworks_base-691e93e82cf97338a760c5fbf900ef29ed9224c2.tar.gz frameworks_base-691e93e82cf97338a760c5fbf900ef29ed9224c2.tar.bz2 |
TM TaskServiceContext implementation
Each task is run on the client from within a TaskServiceContext.
TSC tracks the state of execution of each task on the client.
Change-Id: I93c306a83c1115559f4e9675d9997dceae3f186a
Diffstat (limited to 'services')
-rw-r--r-- | services/core/java/com/android/server/task/TaskCompletedListener.java | 38 | ||||
-rw-r--r-- | services/core/java/com/android/server/task/TaskManagerService.java | 131 | ||||
-rw-r--r-- | services/core/java/com/android/server/task/TaskServiceContext.java | 459 | ||||
-rw-r--r-- | services/core/java/com/android/server/task/TaskStore.java (renamed from services/core/java/com/android/server/task/TaskList.java) | 59 | ||||
-rw-r--r-- | services/core/java/com/android/server/task/controllers/ConnectivityController.java | 33 | ||||
-rw-r--r-- | services/core/java/com/android/server/task/controllers/TaskStatus.java | 34 |
6 files changed, 674 insertions, 80 deletions
diff --git a/services/core/java/com/android/server/task/TaskCompletedListener.java b/services/core/java/com/android/server/task/TaskCompletedListener.java new file mode 100644 index 0000000..0210442 --- /dev/null +++ b/services/core/java/com/android/server/task/TaskCompletedListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014 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.server.task; + +/** + * Used for communication between {@link com.android.server.task.TaskServiceContext} and the + * {@link com.android.server.task.TaskManagerService}. + */ +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); +} diff --git a/services/core/java/com/android/server/task/TaskManagerService.java b/services/core/java/com/android/server/task/TaskManagerService.java index 5df4b2a..1b3a927 100644 --- a/services/core/java/com/android/server/task/TaskManagerService.java +++ b/services/core/java/com/android/server/task/TaskManagerService.java @@ -17,16 +17,15 @@ package com.android.server.task; import android.content.Context; +import android.content.Task; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Log; import android.util.SparseArray; import com.android.server.task.controllers.TaskStatus; -import java.util.ArrayList; -import java.util.List; - /** * Responsible for taking tasks representing work to be performed by a client app, and determining * based on the criteria specified when that task should be run against the client application's @@ -34,25 +33,29 @@ import java.util.List; * @hide */ public class TaskManagerService extends com.android.server.SystemService - implements StateChangedListener { + implements StateChangedListener, TaskCompletedListener { + static final String TAG = "TaskManager"; /** Master list of tasks. */ - private final TaskList mTaskList; + private final TaskStore mTasks; + + /** 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; /** * Track Services that have currently active or pending tasks. The index is provided by * {@link TaskStatus#getServiceToken()} */ - private final SparseArray<TaskServiceContext> mPendingTaskServices = + private final SparseArray<TaskServiceContext> mActiveServices = new SparseArray<TaskServiceContext>(); private final TaskHandler mHandler; private class TaskHandler extends Handler { - /** 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; public TaskHandler(Looper looper) { super(looper); @@ -67,21 +70,42 @@ public class TaskManagerService extends com.android.server.SystemService case MSG_STOP_TASK: break; + case MSG_CHECK_TASKS: + checkTasks(); + break; } } /** - * Helper to post a message to this handler that will run through the pending queue and - * start any tasks it can. + * Called when we need to run through the list of all tasks and start/stop executing one or + * more of them. */ - void sendRunPendingTasksMessage() { - Message m = Message.obtain(this, MSG_RUN_PENDING); - m.sendToTarget(); + 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); + } + } + } } + } - void sendOnStopMessage(TaskStatus taskStatus) { - - } + /** + * 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. + */ + public int schedule(Task task, int userId, int uId, boolean canPersistTask) { + TaskStatus taskStatus = mTasks.addNewTaskForUser(task, userId, uId, canPersistTask); + return 0; } /** @@ -95,7 +119,7 @@ public class TaskManagerService extends com.android.server.SystemService */ public TaskManagerService(Context context) { super(context); - mTaskList = new TaskList(); + mTasks = new TaskStore(context); mHandler = new TaskHandler(context.getMainLooper()); } @@ -104,25 +128,80 @@ public class TaskManagerService extends com.android.server.SystemService } + // StateChangedListener implementations. + /** - * Offboard work to our handler thread as quickly as possible, b/c this call is probably being + * 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 taskStatus The state of the task which has changed. */ @Override public void onTaskStateChanged(TaskStatus taskStatus) { - if (taskStatus.isReady()) { + postCheckTasksMessage(); - } else { - if (mPendingTaskServices.get(taskStatus.getServiceToken()) != null) { - // The task is either pending or being executed, which we have to cancel. - } + } + + @Override + public void onTaskDeadlineExpired(TaskStatus taskStatus) { + + } + + // TaskCompletedListener implementations. + + /** + * 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 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) { + Log.e(TAG, "Task completed for invalid service context; " + serviceToken); + return; } } @Override - public void onTaskDeadlineExpired(TaskStatus taskStatus) { + public void onClientExecutionCompleted(int serviceToken) { + + } + 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); + } + 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; + } + 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(); } } diff --git a/services/core/java/com/android/server/task/TaskServiceContext.java b/services/core/java/com/android/server/task/TaskServiceContext.java index 65c6fa5..2d148d5 100644 --- a/services/core/java/com/android/server/task/TaskServiceContext.java +++ b/services/core/java/com/android/server/task/TaskServiceContext.java @@ -16,79 +16,500 @@ package com.android.server.task; +import android.app.ActivityManager; import android.app.task.ITaskCallback; import android.app.task.ITaskService; +import android.app.task.TaskParams; import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; import android.content.ServiceConnection; -import android.content.Task; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.WorkSource; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; 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 - * can then be reused to start concurrent tasks on the TaskService. Information here is unique - * within this service. + * is reused to start concurrent tasks on the TaskService. Information here is unique + * to the service. * Functionality provided by this class: * - Managages wakelock for the service. * - Sends onStartTask() and onStopTask() messages to client app, and handles callbacks. * - */ public class TaskServiceContext extends ITaskCallback.Stub implements ServiceConnection { + private static final String TAG = "TaskServiceContext"; + /** Define the maximum # of tasks allowed to run on a service at once. */ + private static final int defaultMaxActiveTasksPerService = + ActivityManager.isLowRamDeviceStatic() ? 1 : 3; + /** Amount of time a task is allowed to execute for before being considered timed-out. */ + private static final long EXECUTING_TIMESLICE_MILLIS = 5 * 60 * 1000; + /** Amount of time the TaskManager will wait for a response from an app for a message. */ + private static final long OP_TIMEOUT_MILLIS = 8 * 1000; + /** String prefix for all wakelock names. */ + private static final String TM_WAKELOCK_PREFIX = "*task*/"; + + private static final String[] VERB_STRINGS = { + "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_PENDING" + }; + + // 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; + + // Messages that result from interactions with the client service. + /** System timed out waiting for a response. */ + private static final int MSG_TIMEOUT = 0; + /** 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_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; - int uid; + final int userId; ITaskService service; + private final Handler mCallbackHandler; + /** Tasks that haven't been sent to the client for execution yet. */ + private final SparseArray<ActiveTask> mPending; + /** 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; /** Whether this service is actively bound. */ boolean mBound; - TaskServiceContext(Task task) { - this.component = task.getService(); + 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); } - public void stopTask() { - + @Override + public void taskFinished(int taskId, boolean reschedule) { + mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0) + .sendToTarget(); } - public void startTask(Task task) { - + @Override + public void acknowledgeStopMessage(int taskId, boolean reschedule) { + mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0) + .sendToTarget(); } @Override - public void taskFinished(int taskId, boolean reschedule) { - + public void acknowledgeStartMessage(int taskId, boolean ongoing) { + mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, ongoing ? 1 : 0).sendToTarget(); } - @Override - public void acknowledgeStopMessage(int taskId) { + /** + * 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. + */ + 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); + } + } + } + /** + * 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(); } - @Override - public void acknowledgeStartMessage(int taskId) { + /** + * 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. + */ + 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"); + } + 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) { - return true; + synchronized (mPending) { + return mPending.get(taskStatus.getTaskId()) != null; + } } public boolean isBound() { return mBound; } + /** + * We acquire/release the 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 service The IBinder of the Service's communication channel, + */ @Override public void onServiceConnected(ComponentName name, IBinder service) { - mBound = true; + 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. + mCallbackHandler.removeMessages(MSG_TIMEOUT); + mWakeLock.acquire(); + mCallbackHandler.obtainMessage(MSG_CHECK_PENDING).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. + * @param name The concrete component name of the service whose + */ @Override public void onServiceDisconnected(ComponentName name) { - mBound = false; + // Service disconnected... probably client crashed. + startShutdown(); + } + + /** + * 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. + */ + private void startShutdown() { + mCompletedListener.onClientExecutionCompleted(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]; + } + } + + /** + * Handles the lifecycle of the TaskService binding/callbacks, etc. The convention within this + * class is to append 'H' to each function name that can only be called on this handler. This + * isn't strictly necessary because all of these functions are private, but helps clarity. + */ + private class TaskServiceHandler extends Handler { + TaskServiceHandler(Looper looper) { + super(looper); + } + + @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(); + 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 (receivedCallback.verb == VERB_STARTING) { + final boolean workOngoing = message.arg2 == 1; + handleStartedH(receivedCallback, workOngoing); + } else if (receivedCallback.verb == VERB_EXECUTING || + receivedCallback.verb == VERB_STOPPING) { + final boolean reschedule = message.arg2 == 1; + handleFinishedH(receivedCallback, reschedule); + } else { + if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) { + Log.d(TAG, "Unrecognised callback: " + receivedCallback); + } + } + break; + case MSG_CANCEL: + ActiveTask cancelled = mPending.get(message.arg1); + handleCancelH(cancelled); + 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(); + break; + default: + Log.e(TAG, "Unrecognised message: " + message); + } + } + + /** + * State behaviours. + * VERB_STARTING -> Successful start, change task to VERB_EXECUTING and post timeout. + * _PENDING -> Error + * _EXECUTING -> Error + * _STOPPING -> Error + */ + private void handleStartedH(ActiveTask started, boolean workOngoing) { + switch (started.verb) { + case VERB_STARTING: + started.verb = VERB_EXECUTING; + if (!workOngoing) { + // Task is finished already so fast-forward to handleFinished. + handleFinishedH(started, false); + return; + } else if (started.cancelled.get()) { + // Cancelled *while* waiting for acknowledgeStartMessage from client. + handleCancelH(started); + return; + } else { + scheduleOpTimeOut(started); + } + break; + default: + Log.e(TAG, "Handling started task but task wasn't starting! " + started); + return; + } + } + + /** + * VERB_EXECUTING -> Client called taskFinished(), clean up and notify done. + * _STOPPING -> Successful finish, clean up and notify done. + * _STARTING -> Error + * _PENDING -> Error + */ + private void handleFinishedH(ActiveTask executedTask, boolean reschedule) { + switch (executedTask.verb) { + case VERB_EXECUTING: + case VERB_STOPPING: + closeAndCleanupTaskH(executedTask, reschedule); + break; + default: + Log.e(TAG, "Got an execution complete message for a task that wasn't being" + + "executed. " + executedTask); + } + } + + /** + * 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)}. + * _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; + case VERB_STARTING: + cancelledTask.cancelled.set(true); + break; + case VERB_EXECUTING: + cancelledTask.verb = VERB_STOPPING; + sendStopMessageH(cancelledTask); + break; + case VERB_STOPPING: + // Nada. + break; + default: + Log.e(TAG, "Cancelling a task without a valid verb: " + cancelledTask); + 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#onClientExecutionCompleted(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) { + if (Log.isLoggable(TaskManagerService.TAG, Log.DEBUG)) { + Log.d(TAG, "MSG_TIMEOUT of " + component.getShortClassName() + " : " + + timedOutTask.params.getTaskId()); + } + + final int taskId = timedOutTask.params.getTaskId(); + switch (timedOutTask.verb) { + 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 */); + 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 */); + 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); + break; + default: + Log.e(TAG, "Handling timeout for an unknown active task state: " + + timedOutTask); + 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. + */ + 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? + return; + } + try { + service.stopTask(stoppingTask.params); + stoppingTask.verb = VERB_STOPPING; + scheduleOpTimeOut(stoppingTask); + } 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); + } + } + + /** + * The provided task has finished, either by calling + * {@link android.app.task.TaskService#taskFinished(android.app.task.TaskParams, boolean)} + * 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(); + } + mCompletedListener.onTaskCompleted(token, completedTask.params.getTaskId(), reschedule); + } } } diff --git a/services/core/java/com/android/server/task/TaskList.java b/services/core/java/com/android/server/task/TaskStore.java index d2b8440..3bfc8a5 100644 --- a/services/core/java/com/android/server/task/TaskList.java +++ b/services/core/java/com/android/server/task/TaskStore.java @@ -16,16 +16,12 @@ package com.android.server.task; -import android.content.ComponentName; +import android.content.Context; import android.content.Task; +import android.util.SparseArray; import com.android.server.task.controllers.TaskStatus; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Maintain a list of classes, and accessor methods/logic for these tasks. * This class offers the following functionality: @@ -36,25 +32,39 @@ import java.util.Map; * - Handles rescheduling of tasks. * - When a periodic task is executed and must be re-added. * - When a task fails and the client requests that it be retried with backoff. + * - This class is <strong>not</strong> thread-safe. */ -public class TaskList { +public class TaskStore { - final List<TaskStatus> mTasks; + /** + * Master list, indexed by {@link com.android.server.task.controllers.TaskStatus#hashCode()}. + */ + final SparseArray<TaskStatus> mTasks; + final Context mContext; - TaskList() { + TaskStore(Context context) { mTasks = intialiseTaskMapFromDisk(); + mContext = context; } /** * 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. - * @param persistable true if the TaskQueue should persist this task to the disk. - * @return true if this operation was successful. If false, this task was neither added nor - * persisted. + * @return The initialised TaskStatus object if this operation was successful, null if it + * failed. */ - // TODO: implement this when i decide whether i want to key by TaskStatus - public boolean add(Task task, boolean persistable) { - return true; + 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); + } + } + return taskStatus; } /** @@ -68,11 +78,26 @@ public class TaskList { } /** + * Every time the state changes we write all the tasks in one swathe, instead of trying to + * track incremental changes. + */ + private boolean writeStatusToDisk() { + return true; + } + + /** * * @return */ // TODO: Implement this. - private List<TaskStatus> intialiseTaskMapFromDisk() { - return new ArrayList<TaskStatus>(); + private SparseArray<TaskStatus> intialiseTaskMapFromDisk() { + return new SparseArray<TaskStatus>(); + } + + /** + * @return The live array of TaskStatus objects. + */ + public SparseArray<TaskStatus> getTasks() { + return mTasks; } } 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 5cca77c..fad41d9 100644 --- a/services/core/java/com/android/server/task/controllers/ConnectivityController.java +++ b/services/core/java/com/android/server/task/controllers/ConnectivityController.java @@ -41,6 +41,11 @@ public class ConnectivityController extends StateController { private final BroadcastReceiver mConnectivityChangedReceiver = new ConnectivityChangedReceiver(); + /** 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) { super(service); // Register connectivity changed BR. @@ -51,31 +56,30 @@ public class ConnectivityController extends StateController { } @Override - public void maybeTrackTaskState(TaskStatus taskStatus) { + public void maybeStartTrackingTask(TaskStatus taskStatus) { if (taskStatus.hasConnectivityConstraint() || taskStatus.hasMeteredConstraint()) { + taskStatus.connectivityConstraintSatisfied.set(mConnectivity); + taskStatus.meteredConstraintSatisfied.set(mMetered); mTrackedTasks.add(taskStatus); } } @Override - public void removeTaskStateIfTracked(TaskStatus taskStatus) { + public void maybeStopTrackingTask(TaskStatus taskStatus) { mTrackedTasks.remove(taskStatus); } /** - * @param isConnected Whether the active network is connected for the given uid - * @param isMetered Whether the active network is metered for the given uid. This is - * necessarily false if <code>isConnected</code> is false. * @param userId Id of the user for whom we are updating the connectivity state. */ - private void updateTrackedTasks(boolean isConnected, boolean isMetered, int userId) { + private void updateTrackedTasks(int userId) { for (TaskStatus ts : mTrackedTasks) { if (ts.userId != userId) { continue; } - boolean prevIsConnected = ts.connectivityConstraintSatisfied.getAndSet(isConnected); - boolean prevIsMetered = ts.meteredConstraintSatisfied.getAndSet(isMetered); - if (prevIsConnected != isConnected || prevIsMetered != isMetered) { + boolean prevIsConnected = ts.connectivityConstraintSatisfied.getAndSet(mConnectivity); + boolean prevIsMetered = ts.meteredConstraintSatisfied.getAndSet(mMetered); + if (prevIsConnected != mConnectivity || prevIsMetered != mMetered) { mStateChangedListener.onTaskStateChanged(ts); } } @@ -83,12 +87,13 @@ public class ConnectivityController extends StateController { class ConnectivityChangedReceiver extends BroadcastReceiver { /** - * We'll receive connectivity changes for each user here, which we'll process independently. + * We'll receive connectivity changes for each user here, which we process independently. * We are only interested in the active network here. We're only interested in the active * network, b/c the end result of this will be for apps to try to hit the network. * @param context The Context in which the receiver is running. * @param intent The Intent being received. */ + // TODO: Test whether this will be called twice for each user. @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -103,13 +108,13 @@ public class ConnectivityController extends StateController { // This broadcast gets sent a lot, only update if the active network has changed. if (activeNetwork.getType() == networkType) { final int userid = context.getUserId(); - boolean isMetered = false; - boolean isConnected = + mMetered = false; + mConnectivity = !intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); if (isConnected) { // No point making the call if we know there's no conn. - isMetered = connManager.isActiveNetworkMetered(); + mMetered = connManager.isActiveNetworkMetered(); } - updateTrackedTasks(isConnected, isMetered, userid); + updateTrackedTasks(userid); } } else { Log.w(TAG, "Unrecognised action in intent: " + action); 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 230b049..d96fedc 100644 --- a/services/core/java/com/android/server/task/controllers/TaskStatus.java +++ b/services/core/java/com/android/server/task/controllers/TaskStatus.java @@ -18,6 +18,8 @@ package com.android.server.task.controllers; import android.content.ComponentName; import android.content.Task; +import android.content.pm.PackageParser; +import android.os.Bundle; import android.os.SystemClock; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,7 +38,9 @@ import java.util.concurrent.atomic.AtomicBoolean; public class TaskStatus { final int taskId; final int userId; - ComponentName component; + final int uId; + final ComponentName component; + final Bundle extras; final AtomicBoolean chargingConstraintSatisfied = new AtomicBoolean(); final AtomicBoolean timeConstraintSatisfied = new AtomicBoolean(); @@ -60,15 +64,17 @@ public class TaskStatus { /** 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? - static TaskStatus getForTaskAndUid(Task task, int uId) { - return new TaskStatus(task, uId); + 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) { + TaskStatus(Task task, int userId, int uId) { this.taskId = task.getTaskId(); this.userId = userId; this.component = task.getService(); + this.extras = task.getExtras(); + this.uId = uId; hasChargingConstraint = task.isRequireCharging(); hasIdleConstraint = task.isRequireDeviceIdle(); @@ -94,6 +100,26 @@ public class TaskStatus { hasConnectivityConstraint = task.getNetworkCapabilities() == Task.NetworkType.ANY; } + public int getTaskId() { + return taskId; + } + + public ComponentName getServiceComponent() { + return component; + } + + public int getUserId() { + return userId; + } + + public int getUid() { + return uId; + } + + public Bundle getExtras() { + return extras; + } + boolean hasConnectivityConstraint() { return hasConnectivityConstraint; } |