Merge "TM TaskServiceContext implementation"

This commit is contained in:
Matthew Williams
2014-05-16 20:48:46 +00:00
committed by Android (Google) Code Review
8 changed files with 721 additions and 127 deletions

View File

@@ -34,14 +34,17 @@ interface ITaskCallback {
* Immediate callback to the system after sending a start signal, used to quickly detect ANR.
*
* @param taskId Unique integer used to identify this task.
* @param ongoing True to indicate that the client is processing the task. False if the task is
* complete
*/
void acknowledgeStartMessage(int taskId);
void acknowledgeStartMessage(int taskId, boolean ongoing);
/**
* Immediate callback to the system after sending a stop signal, used to quickly detect ANR.
*
* @param taskId Unique integer used to identify this task.
* @param rescheulde Whether or not to reschedule this task.
*/
void acknowledgeStopMessage(int taskId);
void acknowledgeStopMessage(int taskId, boolean reschedule);
/*
* Tell the task manager that the client is done with its execution, so that it can go on to
* the next one and stop attributing wakelock time to us etc.

View File

@@ -18,7 +18,6 @@ package android.app.task;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -124,22 +123,20 @@ public abstract class TaskService extends Service {
switch (msg.what) {
case MSG_EXECUTE_TASK:
try {
TaskService.this.onStartTask(params);
boolean workOngoing = TaskService.this.onStartTask(params);
ackStartMessage(params, workOngoing);
} catch (Exception e) {
Log.e(TAG, "Error while executing task: " + params.getTaskId());
throw new RuntimeException(e);
} finally {
maybeAckMessageReceived(params, MSG_EXECUTE_TASK);
}
break;
case MSG_STOP_TASK:
try {
TaskService.this.onStopTask(params);
boolean ret = TaskService.this.onStopTask(params);
ackStopMessage(params, ret);
} catch (Exception e) {
Log.e(TAG, "Application unable to handle onStopTask.", e);
throw new RuntimeException(e);
} finally {
maybeAckMessageReceived(params, MSG_STOP_TASK);
}
break;
case MSG_TASK_FINISHED:
@@ -162,30 +159,34 @@ public abstract class TaskService extends Service {
}
}
/**
* Messages come in on the application's main thread, so rather than run the risk of
* waiting for an app that may be doing something foolhardy, we ack to the system after
* processing a message. This allows us to throw up an ANR dialogue as quickly as possible.
* @param params id of the task we're acking.
* @param state Information about what message we're acking.
*/
private void maybeAckMessageReceived(TaskParams params, int state) {
private void ackStartMessage(TaskParams params, boolean workOngoing) {
final ITaskCallback callback = params.getCallback();
final int taskId = params.getTaskId();
if (callback != null) {
try {
if (state == MSG_EXECUTE_TASK) {
callback.acknowledgeStartMessage(taskId);
} else if (state == MSG_STOP_TASK) {
callback.acknowledgeStopMessage(taskId);
}
callback.acknowledgeStartMessage(taskId, workOngoing);
} catch(RemoteException e) {
Log.e(TAG, "System unreachable for starting task.");
}
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, state + ": Attempting to ack a task that has already been" +
"processed.");
Log.d(TAG, "Attempting to ack a task that has already been processed.");
}
}
}
private void ackStopMessage(TaskParams params, boolean reschedule) {
final ITaskCallback callback = params.getCallback();
final int taskId = params.getTaskId();
if (callback != null) {
try {
callback.acknowledgeStopMessage(taskId, reschedule);
} catch(RemoteException e) {
Log.e(TAG, "System unreachable for stopping task.");
}
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Attempting to ack a task that has already been processed.");
}
}
}
@@ -203,12 +204,14 @@ public abstract class TaskService extends Service {
*
* @param params Parameters specifying info about this task, including the extras bundle you
* optionally provided at task-creation time.
* @return True if your service needs to process the work (on a separate thread). False if
* there's no more work to be done for this task.
*/
public abstract void onStartTask(TaskParams params);
public abstract boolean onStartTask(TaskParams params);
/**
* This method is called if your task should be stopped even before you've called
* {@link #taskFinished(TaskParams, boolean)}.
* This method is called if the system has determined that you must stop execution of your task
* even before you've had a chance to call {@link #taskFinished(TaskParams, boolean)}.
*
* <p>This will happen if the requirements specified at schedule time are no longer met. For
* example you may have requested WiFi with
@@ -217,33 +220,27 @@ public abstract class TaskService extends Service {
* {@link android.content.Task.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its
* idle maintenance window. You are solely responsible for the behaviour of your application
* upon receipt of this message; your app will likely start to misbehave if you ignore it. One
* repercussion is that the system will cease to hold a wakelock for you.</p>
*
* <p>After you've done your clean-up you are still expected to call
* {@link #taskFinished(TaskParams, boolean)} this will inform the TaskManager that all is well, and
* allow you to reschedule your task as it is probably uncompleted. Until you call
* taskFinished() you will not receive any newly scheduled tasks with the given task id as the
* TaskManager will consider the task to be in an error state.</p>
* immediate repercussion is that the system will cease holding a wakelock for you.</p>
*
* @param params Parameters specifying info about this task.
* @return True to indicate to the TaskManager whether you'd like to reschedule this task based
* on the criteria provided at task creation-time. False to drop the task. Regardless of the
* value returned, your task must stop executing.
* on the retry criteria provided at task creation-time. False to drop the task. Regardless of
* the value returned, your task must stop executing.
*/
public abstract boolean onStopTask(TaskParams params);
/**
* Callback to inform the TaskManager you have completed execution. This can be called from any
* Callback to inform the TaskManager you've finished executing. This can be called from any
* thread, as it will ultimately be run on your application's main thread. When the system
* receives this message it will release the wakelock being held.
* <p>
* You can specify post-execution behaviour to the scheduler here with <code>needsReschedule
* </code>. This will apply a back-off timer to your task based on the default, or what was
* set with {@link android.content.Task.Builder#setBackoffCriteria(long, int)}. The
* original requirements are always honoured even for a backed-off task.
* Note that a task running in idle mode will not be backed-off. Instead what will happen
* is the task will be re-added to the queue and re-executed within a future idle
* maintenance window.
* You can specify post-execution behaviour to the scheduler here with
* <code>needsReschedule </code>. This will apply a back-off timer to your task based on
* the default, or what was set with
* {@link android.content.Task.Builder#setBackoffCriteria(long, int)}. The original
* requirements are always honoured even for a backed-off task. Note that a task running in
* idle mode will not be backed-off. Instead what will happen is the task will be re-added
* to the queue and re-executed within a future idle maintenance window.
* </p>
*
* @param params Parameters specifying system-provided info about this task, this was given to

View File

@@ -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);
}

View File

@@ -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);
@@ -66,22 +69,43 @@ public class TaskManagerService extends com.android.server.SystemService
break;
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,20 +128,19 @@ 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()) {
} else {
if (mPendingTaskServices.get(taskStatus.getServiceToken()) != null) {
// The task is either pending or being executed, which we have to cancel.
}
}
postCheckTasksMessage();
}
@@ -125,4 +148,60 @@ public class TaskManagerService extends com.android.server.SystemService
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 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();
}
}

View File

@@ -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();
}
public void stopTask() {
}
public void startTask(Task task) {
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);
}
@Override
public void taskFinished(int taskId, boolean reschedule) {
mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0)
.sendToTarget();
}
@Override
public void acknowledgeStopMessage(int taskId) {
public void acknowledgeStopMessage(int taskId, boolean reschedule) {
mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, reschedule ? 1 : 0)
.sendToTarget();
}
@Override
public void acknowledgeStartMessage(int taskId) {
public void acknowledgeStartMessage(int taskId, boolean ongoing) {
mCallbackHandler.obtainMessage(MSG_CALLBACK, taskId, ongoing ? 1 : 0).sendToTarget();
}
/**
* 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();
}
/**
* 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);
}
}
}

View File

@@ -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;
}
/**
@@ -67,12 +77,27 @@ 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;
}
}

View File

@@ -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);

View File

@@ -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;
}