summaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorMatthew Williams <mjwilliams@google.com>2014-06-05 20:08:54 +0000
committerAndroid Git Automerger <android-git-automerger@android.com>2014-06-05 20:08:54 +0000
commit38b200fe6c1aba63b57b315f4274d337545f0c89 (patch)
treeb35feade7894ab5e8d97d4269658250c9a0d33e4 /services
parented50e94d6c47a99c9dae0b950a258c074576493e (diff)
parentafcece8115d89802b0618dacf8a6a18fc5a349b1 (diff)
downloadframeworks_base-38b200fe6c1aba63b57b315f4274d337545f0c89.zip
frameworks_base-38b200fe6c1aba63b57b315f4274d337545f0c89.tar.gz
frameworks_base-38b200fe6c1aba63b57b315f4274d337545f0c89.tar.bz2
am 1a2f47d4: Merge "Add persistence of tasks for TaskManager & BatteryController" into lmp-preview-dev
* commit '1a2f47d4cdc0ea40dff1b88f7976d64c19d687b1': Add persistence of tasks for TaskManager & BatteryController
Diffstat (limited to 'services')
-rw-r--r--services/core/java/com/android/server/task/TaskManagerService.java56
-rw-r--r--services/core/java/com/android/server/task/TaskStore.java574
-rw-r--r--services/core/java/com/android/server/task/controllers/BatteryController.java213
-rw-r--r--services/core/java/com/android/server/task/controllers/ConnectivityController.java8
-rw-r--r--services/core/java/com/android/server/task/controllers/IdleController.java8
-rw-r--r--services/core/java/com/android/server/task/controllers/StateController.java8
-rw-r--r--services/core/java/com/android/server/task/controllers/TaskStatus.java57
-rw-r--r--services/core/java/com/android/server/task/controllers/TimeController.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java199
-rw-r--r--services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java66
10 files changed, 1102 insertions, 94 deletions
diff --git a/services/core/java/com/android/server/task/TaskManagerService.java b/services/core/java/com/android/server/task/TaskManagerService.java
index d5b70e6..a5f865f 100644
--- a/services/core/java/com/android/server/task/TaskManagerService.java
+++ b/services/core/java/com/android/server/task/TaskManagerService.java
@@ -36,6 +36,7 @@ import android.os.SystemClock;
import android.util.Slog;
import android.util.SparseArray;
+import com.android.server.task.controllers.BatteryController;
import com.android.server.task.controllers.ConnectivityController;
import com.android.server.task.controllers.IdleController;
import com.android.server.task.controllers.StateController;
@@ -48,12 +49,19 @@ import java.util.LinkedList;
* 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
* endpoint.
+ * Implements logic for scheduling, and rescheduling tasks. The TaskManagerService knows nothing
+ * about constraints, or the state of active tasks. It receives callbacks from the various
+ * controllers and completed tasks and operates accordingly.
+ *
+ * Note on locking: Any operations that manipulate {@link #mTasks} need to lock on that object, and
+ * similarly for {@link #mActiveServices}. If both locks need to be held take mTasksSet first and then
+ * mActiveService afterwards.
* @hide
*/
public class TaskManagerService extends com.android.server.SystemService
- implements StateChangedListener, TaskCompletedListener {
+ implements StateChangedListener, TaskCompletedListener, TaskMapReadFinishedListener {
// TODO: Switch this off for final version.
- private static final boolean DEBUG = true;
+ 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";
@@ -113,8 +121,8 @@ public class TaskManagerService extends com.android.server.SystemService
*/
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;
+ startTrackingTask(taskStatus);
+ return TaskManager.RESULT_SUCCESS;
}
public List<Task> getPendingTasks(int uid) {
@@ -210,7 +218,7 @@ public class TaskManagerService extends com.android.server.SystemService
*/
public TaskManagerService(Context context) {
super(context);
- mTasks = new TaskStore(context);
+ mTasks = TaskStore.initAndGet(this);
mHandler = new TaskHandler(context.getMainLooper());
mTaskManagerStub = new TaskManagerStub();
// Create the "runners".
@@ -218,12 +226,12 @@ public class TaskManagerService extends com.android.server.SystemService
mActiveServices.add(
new TaskServiceContext(this, context.getMainLooper()));
}
-
+ // Create the controllers.
mControllers = new LinkedList<StateController>();
mControllers.add(ConnectivityController.get(this));
mControllers.add(TimeController.get(this));
mControllers.add(IdleController.get(this));
- // TODO: Add BatteryStateController when implemented.
+ mControllers.add(BatteryController.get(this));
}
@Override
@@ -236,17 +244,14 @@ public class TaskManagerService extends com.android.server.SystemService
* {@link com.android.server.task.TaskStore}, and make sure all the relevant controllers know
* about.
*/
- private boolean startTrackingTask(TaskStatus taskStatus) {
- boolean added = false;
+ private void startTrackingTask(TaskStatus taskStatus) {
synchronized (mTasks) {
- added = mTasks.add(taskStatus);
+ mTasks.add(taskStatus);
}
- if (added) {
- for (StateController controller : mControllers) {
- controller.maybeStartTrackingTask(taskStatus);
- }
+ for (StateController controller : mControllers) {
+ controller.maybeStartTrackingTask(taskStatus);
+
}
- return added;
}
/**
@@ -404,6 +409,27 @@ public class TaskManagerService extends com.android.server.SystemService
mHandler.obtainMessage(MSG_TASK_EXPIRED, taskStatus);
}
+ /**
+ * Disk I/O is finished, take the list of tasks we read from disk and add them to our
+ * {@link TaskStore}.
+ * This is run on the {@link com.android.server.IoThread} instance, which is a separate thread,
+ * and is called once at boot.
+ */
+ @Override
+ public void onTaskMapReadFinished(List<TaskStatus> tasks) {
+ synchronized (mTasks) {
+ for (TaskStatus ts : tasks) {
+ if (mTasks.contains(ts)) {
+ // An app with BOOT_COMPLETED *might* have decided to reschedule their task, in
+ // the same amount of time it took us to read it from disk. If this is the case
+ // we leave it be.
+ continue;
+ }
+ startTrackingTask(ts);
+ }
+ }
+ }
+
private class TaskHandler extends Handler {
public TaskHandler(Looper looper) {
diff --git a/services/core/java/com/android/server/task/TaskStore.java b/services/core/java/com/android/server/task/TaskStore.java
index f72ab22..6bb00b1 100644
--- a/services/core/java/com/android/server/task/TaskStore.java
+++ b/services/core/java/com/android/server/task/TaskStore.java
@@ -16,17 +16,37 @@
package com.android.server.task;
+import android.content.ComponentName;
import android.app.task.Task;
import android.content.Context;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
+import android.util.AtomicFile;
import android.util.ArraySet;
+import android.util.Pair;
import android.util.Slog;
-import android.util.SparseArray;
+import android.util.Xml;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.server.IoThread;
import com.android.server.task.controllers.TaskStatus;
-import java.util.HashSet;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Iterator;
-import java.util.Set;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
/**
* Maintain a list of classes, and accessor methods/logic for these tasks.
@@ -38,57 +58,108 @@ import java.util.Set;
* - 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.
+ * - This class <strong>is not</strong> thread-safe.
+ *
+ * Note on locking:
+ * All callers to this class must <strong>lock on the class object they are calling</strong>.
+ * This is important b/c {@link com.android.server.task.TaskStore.WriteTasksMapToDiskRunnable}
+ * and {@link com.android.server.task.TaskStore.ReadTaskMapFromDiskRunnable} lock on that
+ * object.
*/
public class TaskStore {
private static final String TAG = "TaskManagerStore";
+ private static final boolean DEBUG = TaskManagerService.DEBUG;
+
/** 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 ArraySet<TaskStatus> mTasksSet;
final Context mContext;
private int mDirtyOperations;
- TaskStore(Context context) {
- mTasks = intialiseTasksFromDisk();
+ private static final Object sSingletonLock = new Object();
+ private final AtomicFile mTasksFile;
+ /** Handler backed by IoThread for writing to disk. */
+ private final Handler mIoHandler = IoThread.getHandler();
+ private static TaskStore sSingleton;
+
+ /** Used by the {@Link TaskManagerService} to instantiate the TaskStore. */
+ static TaskStore initAndGet(TaskManagerService taskManagerService) {
+ synchronized (sSingletonLock) {
+ if (sSingleton == null) {
+ sSingleton = new TaskStore(taskManagerService.getContext(),
+ Environment.getDataDirectory(), taskManagerService);
+ }
+ return sSingleton;
+ }
+ }
+
+ @VisibleForTesting
+ public static TaskStore initAndGetForTesting(Context context, File dataDir,
+ TaskMapReadFinishedListener callback) {
+ return new TaskStore(context, dataDir, callback);
+ }
+
+ private TaskStore(Context context, File dataDir, TaskMapReadFinishedListener callback) {
mContext = context;
mDirtyOperations = 0;
+
+ File systemDir = new File(dataDir, "system");
+ File taskDir = new File(systemDir, "task");
+ taskDir.mkdirs();
+ mTasksFile = new AtomicFile(new File(taskDir, "tasks.xml"));
+
+ mTasksSet = new ArraySet<TaskStatus>();
+
+ readTaskMapFromDiskAsync(callback);
}
/**
* 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.
+ * @return Whether or not an equivalent TaskStatus was replaced by this operation.
*/
public boolean add(TaskStatus taskStatus) {
+ boolean replaced = mTasksSet.remove(taskStatus);
+ mTasksSet.add(taskStatus);
if (taskStatus.isPersisted()) {
- if (!maybeWriteStatusToDisk()) {
- return false;
- }
+ maybeWriteStatusToDiskAsync();
}
- mTasks.remove(taskStatus);
- mTasks.add(taskStatus);
- return true;
+ return replaced;
+ }
+
+ /**
+ * Whether this taskStatus object already exists in the TaskStore.
+ */
+ public boolean contains(TaskStatus taskStatus) {
+ return mTasksSet.contains(taskStatus);
}
public int size() {
- return mTasks.size();
+ return mTasksSet.size();
}
/**
* 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.
+ * @return Whether or not the task existed to be removed.
*/
public boolean remove(TaskStatus taskStatus) {
- boolean removed = mTasks.remove(taskStatus);
+ boolean removed = mTasksSet.remove(taskStatus);
if (!removed) {
- Slog.e(TAG, "Error removing task: " + taskStatus);
+ if (DEBUG) {
+ Slog.d(TAG, "Couldn't remove task: didn't exist: " + taskStatus);
+ }
return false;
- } else {
- maybeWriteStatusToDisk();
}
- return true;
+ maybeWriteStatusToDiskAsync();
+ return removed;
+ }
+
+ @VisibleForTesting
+ public void clear() {
+ mTasksSet.clear();
+ maybeWriteStatusToDiskAsync();
}
/**
@@ -100,19 +171,16 @@ public class TaskStore {
* was found.
*/
public boolean removeAllByUid(int uid) {
- Iterator<TaskStatus> it = mTasks.iterator();
- boolean removed = false;
+ Iterator<TaskStatus> it = mTasksSet.iterator();
while (it.hasNext()) {
TaskStatus ts = it.next();
if (ts.getUid() == uid) {
it.remove();
- removed = true;
+ maybeWriteStatusToDiskAsync();
+ return true;
}
}
- if (removed) {
- maybeWriteStatusToDisk();
- }
- return removed;
+ return false;
}
/**
@@ -124,48 +192,464 @@ public class TaskStore {
* @return true if a removal occurred, false if the provided parameters didn't match anything.
*/
public boolean remove(int uid, int taskId) {
- Iterator<TaskStatus> it = mTasks.iterator();
+ boolean changed = false;
+ Iterator<TaskStatus> it = mTasksSet.iterator();
while (it.hasNext()) {
TaskStatus ts = it.next();
if (ts.getUid() == uid && ts.getTaskId() == taskId) {
it.remove();
- maybeWriteStatusToDisk();
- return true;
+ changed = true;
}
}
- return false;
+ if (changed) {
+ maybeWriteStatusToDiskAsync();
+ }
+ return changed;
}
/**
* @return The live array of TaskStatus objects.
*/
- public Set<TaskStatus> getTasks() {
- return mTasks;
+ public ArraySet<TaskStatus> getTasks() {
+ return mTasksSet;
}
+ /** Version of the db schema. */
+ private static final int TASKS_FILE_VERSION = 0;
+ /** Tag corresponds to constraints this task needs. */
+ private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
+ /** Tag corresponds to execution parameters. */
+ private static final String XML_TAG_PERIODIC = "periodic";
+ private static final String XML_TAG_ONEOFF = "one-off";
+ private static final String XML_TAG_EXTRAS = "extras";
+
/**
* 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() {
+ private void maybeWriteStatusToDiskAsync() {
mDirtyOperations++;
- if (mDirtyOperations > MAX_OPS_BEFORE_WRITE) {
- for (TaskStatus ts : mTasks) {
- //
+ if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
+ if (DEBUG) {
+ Slog.v(TAG, "Writing tasks to disk.");
+ }
+ mIoHandler.post(new WriteTasksMapToDiskRunnable());
+ }
+ }
+
+ private void readTaskMapFromDiskAsync(TaskMapReadFinishedListener callback) {
+ mIoHandler.post(new ReadTaskMapFromDiskRunnable(callback));
+ }
+
+ public void readTaskMapFromDisk(TaskMapReadFinishedListener callback) {
+ new ReadTaskMapFromDiskRunnable(callback).run();
+ }
+
+ /**
+ * Runnable that writes {@link #mTasksSet} out to xml.
+ * NOTE: This Runnable locks on TaskStore.this
+ */
+ private class WriteTasksMapToDiskRunnable implements Runnable {
+ @Override
+ public void run() {
+ final long startElapsed = SystemClock.elapsedRealtime();
+ synchronized (TaskStore.this) {
+ writeTasksMapImpl();
+ }
+ if (TaskManagerService.DEBUG) {
+ Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
+ - startElapsed) + "ms");
+ }
+ }
+
+ private void writeTasksMapImpl() {
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(baos, "utf-8");
+ out.startDocument(null, true);
+ out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+ out.startTag(null, "task-info");
+ out.attribute(null, "version", Integer.toString(TASKS_FILE_VERSION));
+ for (int i = 0; i < mTasksSet.size(); i++) {
+ final TaskStatus taskStatus = mTasksSet.valueAt(i);
+ if (DEBUG) {
+ Slog.d(TAG, "Saving task " + taskStatus.getTaskId());
+ }
+ out.startTag(null, "task");
+ addIdentifierAttributesToTaskTag(out, taskStatus);
+ writeConstraintsToXml(out, taskStatus);
+ writeExecutionCriteriaToXml(out, taskStatus);
+ writeBundleToXml(taskStatus.getExtras(), out);
+ out.endTag(null, "task");
+ }
+ out.endTag(null, "task-info");
+ out.endDocument();
+
+ // Write out to disk in one fell sweep.
+ FileOutputStream fos = mTasksFile.startWrite();
+ fos.write(baos.toByteArray());
+ mTasksFile.finishWrite(fos);
+ mDirtyOperations = 0;
+ } catch (IOException e) {
+ if (DEBUG) {
+ Slog.v(TAG, "Error writing out task data.", e);
+ }
+ } catch (XmlPullParserException e) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error persisting bundle.", e);
+ }
+ }
+ }
+
+ /** Write out a tag with data comprising the required fields of this task and its client. */
+ private void addIdentifierAttributesToTaskTag(XmlSerializer out, TaskStatus taskStatus)
+ throws IOException {
+ out.attribute(null, "taskid", Integer.toString(taskStatus.getTaskId()));
+ out.attribute(null, "package", taskStatus.getServiceComponent().getPackageName());
+ out.attribute(null, "class", taskStatus.getServiceComponent().getClassName());
+ out.attribute(null, "uid", Integer.toString(taskStatus.getUid()));
+ }
+
+ private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
+ throws IOException, XmlPullParserException {
+ out.startTag(null, XML_TAG_EXTRAS);
+ extras.saveToXml(out);
+ out.endTag(null, XML_TAG_EXTRAS);
+ }
+ /**
+ * Write out a tag with data identifying this tasks constraints. If the constraint isn't here
+ * it doesn't apply.
+ */
+ private void writeConstraintsToXml(XmlSerializer out, TaskStatus taskStatus) throws IOException {
+ out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ if (taskStatus.hasMeteredConstraint()) {
+ out.attribute(null, "unmetered", Boolean.toString(true));
+ }
+ if (taskStatus.hasConnectivityConstraint()) {
+ out.attribute(null, "connectivity", Boolean.toString(true));
+ }
+ if (taskStatus.hasIdleConstraint()) {
+ out.attribute(null, "idle", Boolean.toString(true));
+ }
+ if (taskStatus.hasChargingConstraint()) {
+ out.attribute(null, "charging", Boolean.toString(true));
+ }
+ out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ }
+
+ private void writeExecutionCriteriaToXml(XmlSerializer out, TaskStatus taskStatus)
+ throws IOException {
+ final Task task = taskStatus.getTask();
+ if (taskStatus.getTask().isPeriodic()) {
+ out.startTag(null, XML_TAG_PERIODIC);
+ out.attribute(null, "period", Long.toString(task.getIntervalMillis()));
+ } else {
+ out.startTag(null, XML_TAG_ONEOFF);
+ }
+
+ if (taskStatus.hasDeadlineConstraint()) {
+ // Wall clock deadline.
+ final long deadlineWallclock = System.currentTimeMillis() +
+ (taskStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
+ out.attribute(null, "deadline", Long.toString(deadlineWallclock));
+ }
+ if (taskStatus.hasTimingDelayConstraint()) {
+ final long delayWallclock = System.currentTimeMillis() +
+ (taskStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
+ out.attribute(null, "delay", Long.toString(delayWallclock));
+ }
+
+ // Only write out back-off policy if it differs from the default.
+ // This also helps the case where the task is idle -> these aren't allowed to specify
+ // back-off.
+ if (taskStatus.getTask().getInitialBackoffMillis() != Task.DEFAULT_INITIAL_BACKOFF_MILLIS
+ || taskStatus.getTask().getBackoffPolicy() != Task.DEFAULT_BACKOFF_POLICY) {
+ out.attribute(null, "backoff-policy", Integer.toString(task.getBackoffPolicy()));
+ out.attribute(null, "initial-backoff", Long.toString(task.getInitialBackoffMillis()));
+ }
+ if (task.isPeriodic()) {
+ out.endTag(null, XML_TAG_PERIODIC);
+ } else {
+ out.endTag(null, XML_TAG_ONEOFF);
}
- mDirtyOperations = 0;
}
- return true;
}
/**
- *
- * @return
+ * Runnable that reads list of persisted task from xml.
+ * NOTE: This Runnable locks on TaskStore.this
*/
- // TODO: Implement this.
- private ArraySet<TaskStatus> intialiseTasksFromDisk() {
- return new ArraySet<TaskStatus>();
+ private class ReadTaskMapFromDiskRunnable implements Runnable {
+ private TaskMapReadFinishedListener mCallback;
+ public ReadTaskMapFromDiskRunnable(TaskMapReadFinishedListener callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void run() {
+ try {
+ List<TaskStatus> tasks;
+ synchronized (TaskStore.this) {
+ tasks = readTaskMapImpl();
+ }
+ if (tasks != null) {
+ mCallback.onTaskMapReadFinished(tasks);
+ }
+ } catch (FileNotFoundException e) {
+ if (TaskManagerService.DEBUG) {
+ Slog.d(TAG, "Could not find tasks file, probably there was nothing to load.");
+ }
+ } catch (XmlPullParserException e) {
+ if (TaskManagerService.DEBUG) {
+ Slog.d(TAG, "Error parsing xml.", e);
+ }
+ } catch (IOException e) {
+ if (TaskManagerService.DEBUG) {
+ Slog.d(TAG, "Error parsing xml.", e);
+ }
+ }
+ }
+
+ private List<TaskStatus> readTaskMapImpl() throws XmlPullParserException, IOException {
+ FileInputStream fis = mTasksFile.openRead();
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, null);
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.START_TAG &&
+ eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next();
+ Slog.d(TAG, parser.getName());
+ }
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ if (DEBUG) {
+ Slog.d(TAG, "No persisted tasks.");
+ }
+ return null;
+ }
+
+ String tagName = parser.getName();
+ if ("task-info".equals(tagName)) {
+ final List<TaskStatus> tasks = new ArrayList<TaskStatus>();
+ // Read in version info.
+ try {
+ int version = Integer.valueOf(parser.getAttributeValue(null, "version"));
+ if (version != TASKS_FILE_VERSION) {
+ Slog.d(TAG, "Invalid version number, aborting tasks file read.");
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Invalid version number, aborting tasks file read.");
+ return null;
+ }
+ eventType = parser.next();
+ do {
+ // Read each <task/>
+ if (eventType == XmlPullParser.START_TAG) {
+ tagName = parser.getName();
+ // Start reading task.
+ if ("task".equals(tagName)) {
+ TaskStatus persistedTask = restoreTaskFromXml(parser);
+ if (persistedTask != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Read out " + persistedTask);
+ }
+ tasks.add(persistedTask);
+ } else {
+ Slog.d(TAG, "Error reading task from file.");
+ }
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+ return tasks;
+ }
+ return null;
+ }
+
+ /**
+ * @param parser Xml parser at the beginning of a "<task/>" tag. The next "parser.next()" call
+ * will take the parser into the body of the task tag.
+ * @return Newly instantiated task holding all the information we just read out of the xml tag.
+ */
+ private TaskStatus restoreTaskFromXml(XmlPullParser parser) throws XmlPullParserException,
+ IOException {
+ Task.Builder taskBuilder;
+ int uid;
+
+ // Read out task identifier attributes.
+ try {
+ taskBuilder = buildBuilderFromXml(parser);
+ uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Error parsing task's required fields, skipping");
+ return null;
+ }
+
+ int eventType;
+ // Read out constraints tag.
+ do {
+ eventType = parser.next();
+ } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
+
+ if (!(eventType == XmlPullParser.START_TAG &&
+ XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
+ // Expecting a <constraints> start tag.
+ return null;
+ }
+ try {
+ buildConstraintsFromXml(taskBuilder, parser);
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading constraints, skipping.");
+ return null;
+ }
+ parser.next(); // Consume </constraints>
+
+ // Read out execution parameters tag.
+ do {
+ eventType = parser.next();
+ } while (eventType == XmlPullParser.TEXT);
+ if (eventType != XmlPullParser.START_TAG) {
+ return null;
+ }
+
+ Pair<Long, Long> runtimes;
+ try {
+ runtimes = buildExecutionTimesFromXml(parser);
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error parsing execution time parameters, skipping.");
+ }
+ return null;
+ }
+
+ if (XML_TAG_PERIODIC.equals(parser.getName())) {
+ try {
+ String val = parser.getAttributeValue(null, "period");
+ taskBuilder.setPeriodic(Long.valueOf(val));
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
+ return null;
+ }
+ } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
+ try {
+ if (runtimes.first != TaskStatus.DEFAULT_EARLIEST_RUNTIME) {
+ taskBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime());
+ }
+ if (runtimes.second != TaskStatus.DEFAULT_LATEST_RUNTIME) {
+ taskBuilder.setOverrideDeadline(
+ runtimes.second - SystemClock.elapsedRealtime());
+ }
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading task execution criteria, skipping.");
+ return null;
+ }
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
+ }
+ // Expecting a parameters start tag.
+ return null;
+ }
+ maybeBuildBackoffPolicyFromXml(taskBuilder, parser);
+
+ parser.nextTag(); // Consume parameters end tag.
+
+ // Read out extras Bundle.
+ do {
+ eventType = parser.next();
+ } while (eventType == XmlPullParser.TEXT);
+ if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error reading extras, skipping.");
+ }
+ return null;
+ }
+
+ PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
+ taskBuilder.setExtras(extras);
+ parser.nextTag(); // Consume </extras>
+
+ return new TaskStatus(taskBuilder.build(), uid, runtimes.first, runtimes.second);
+ }
+
+ private Task.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
+ // Pull out required fields from <task> attributes.
+ int taskId = Integer.valueOf(parser.getAttributeValue(null, "taskid"));
+ String packageName = parser.getAttributeValue(null, "package");
+ String className = parser.getAttributeValue(null, "class");
+ ComponentName cname = new ComponentName(packageName, className);
+
+ return new Task.Builder(taskId, cname);
+ }
+
+ private void buildConstraintsFromXml(Task.Builder taskBuilder, XmlPullParser parser) {
+ String val = parser.getAttributeValue(null, "unmetered");
+ if (val != null) {
+ taskBuilder.setRequiredNetworkCapabilities(Task.NetworkType.UNMETERED);
+ }
+ val = parser.getAttributeValue(null, "connectivity");
+ if (val != null) {
+ taskBuilder.setRequiredNetworkCapabilities(Task.NetworkType.ANY);
+ }
+ val = parser.getAttributeValue(null, "idle");
+ if (val != null) {
+ taskBuilder.setRequiresDeviceIdle(true);
+ }
+ val = parser.getAttributeValue(null, "charging");
+ if (val != null) {
+ taskBuilder.setRequiresCharging(true);
+ }
+ }
+
+ /**
+ * Builds the back-off policy out of the params tag. These attributes may not exist, depending
+ * on whether the back-off was set when the task was first scheduled.
+ */
+ private void maybeBuildBackoffPolicyFromXml(Task.Builder taskBuilder, XmlPullParser parser) {
+ String val = parser.getAttributeValue(null, "initial-backoff");
+ if (val != null) {
+ long initialBackoff = Long.valueOf(val);
+ val = parser.getAttributeValue(null, "backoff-policy");
+ int backoffPolicy = Integer.valueOf(val); // Will throw NFE which we catch higher up.
+ taskBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
+ }
+ }
+
+ /**
+ * Convenience function to read out and convert deadline and delay from xml into elapsed real
+ * time.
+ * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
+ * and the second is the latest elapsed runtime.
+ */
+ private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
+ throws NumberFormatException {
+ // Pull out execution time data.
+ final long nowWallclock = System.currentTimeMillis();
+ final long nowElapsed = SystemClock.elapsedRealtime();
+
+ long earliestRunTimeElapsed = TaskStatus.DEFAULT_EARLIEST_RUNTIME;
+ long latestRunTimeElapsed = TaskStatus.DEFAULT_LATEST_RUNTIME;
+ String val = parser.getAttributeValue(null, "deadline");
+ if (val != null) {
+ long latestRuntimeWallclock = Long.valueOf(val);
+ long maxDelayElapsed =
+ Math.max(latestRuntimeWallclock - nowWallclock, 0);
+ latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
+ }
+ val = parser.getAttributeValue(null, "delay");
+ if (val != null) {
+ long earliestRuntimeWallclock = Long.valueOf(val);
+ long minDelayElapsed =
+ Math.max(earliestRuntimeWallclock - nowWallclock, 0);
+ earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
+
+ }
+ return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
+ }
}
}
diff --git a/services/core/java/com/android/server/task/controllers/BatteryController.java b/services/core/java/com/android/server/task/controllers/BatteryController.java
new file mode 100644
index 0000000..585b41f
--- /dev/null
+++ b/services/core/java/com/android/server/task/controllers/BatteryController.java
@@ -0,0 +1,213 @@
+/*
+ * 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.controllers;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.BatteryProperty;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.BatteryService;
+import com.android.server.task.StateChangedListener;
+import com.android.server.task.TaskManagerService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Simple controller that tracks whether the phone is charging or not. The phone is considered to
+ * be charging when it's been plugged in for more than two minutes, and the system has broadcast
+ * ACTION_BATTERY_OK.
+ */
+public class BatteryController extends StateController {
+ private static final String TAG = "BatteryController";
+
+ private static final Object sCreationLock = new Object();
+ private static volatile BatteryController sController;
+ private static final String ACTION_CHARGING_STABLE =
+ "com.android.server.task.controllers.BatteryController.ACTION_CHARGING_STABLE";
+ /** Wait this long after phone is plugged in before doing any work. */
+ private static final long STABLE_CHARGING_THRESHOLD_MILLIS = 2 * 60 * 1000; // 2 minutes.
+
+ private List<TaskStatus> mTrackedTasks = new ArrayList<TaskStatus>();
+ private ChargingTracker mChargeTracker;
+
+ public static BatteryController get(TaskManagerService taskManagerService) {
+ synchronized (sCreationLock) {
+ if (sController == null) {
+ sController = new BatteryController(taskManagerService,
+ taskManagerService.getContext());
+ }
+ }
+ return sController;
+ }
+
+ @VisibleForTesting
+ public ChargingTracker getTracker() {
+ return mChargeTracker;
+ }
+
+ @VisibleForTesting
+ public static BatteryController getForTesting(StateChangedListener stateChangedListener,
+ Context context) {
+ return new BatteryController(stateChangedListener, context);
+ }
+
+ private BatteryController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
+ mChargeTracker = new ChargingTracker();
+ mChargeTracker.startTracking();
+ }
+
+ @Override
+ public void maybeStartTrackingTask(TaskStatus taskStatus) {
+ if (taskStatus.hasChargingConstraint()) {
+ synchronized (mTrackedTasks) {
+ mTrackedTasks.add(taskStatus);
+ taskStatus.chargingConstraintSatisfied.set(mChargeTracker.isOnStablePower());
+ }
+ }
+
+ }
+
+ @Override
+ public void maybeStopTrackingTask(TaskStatus taskStatus) {
+ if (taskStatus.hasChargingConstraint()) {
+ synchronized (mTrackedTasks) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+ }
+
+ private void maybeReportNewChargingState() {
+ final boolean stablePower = mChargeTracker.isOnStablePower();
+ boolean reportChange = false;
+ synchronized (mTrackedTasks) {
+ for (TaskStatus ts : mTrackedTasks) {
+ boolean previous = ts.chargingConstraintSatisfied.getAndSet(stablePower);
+ if (previous != stablePower) {
+ reportChange = true;
+ }
+ }
+ }
+ if (reportChange) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ public class ChargingTracker extends BroadcastReceiver {
+ private final AlarmManager mAlarm;
+ private final PendingIntent mStableChargingTriggerIntent;
+ /**
+ * Track whether we're "charging", where charging means that we're ready to commit to
+ * doing work.
+ */
+ private boolean mCharging;
+ /** Keep track of whether the battery is charged enough that we want to do work. */
+ private boolean mBatteryHealthy;
+
+ public ChargingTracker() {
+ mAlarm = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(ACTION_CHARGING_STABLE)
+ .setComponent(new ComponentName(mContext, this.getClass()));
+ mStableChargingTriggerIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ }
+
+ public void startTracking() {
+ IntentFilter filter = new IntentFilter();
+
+ // Battery health.
+ filter.addAction(Intent.ACTION_BATTERY_LOW);
+ filter.addAction(Intent.ACTION_BATTERY_OKAY);
+ // Charging/not charging.
+ filter.addAction(Intent.ACTION_POWER_CONNECTED);
+ filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+ mContext.registerReceiver(this, filter);
+
+ // Initialise tracker state.
+ BatteryService batteryService = (BatteryService) ServiceManager.getService("battery");
+ if (batteryService != null) {
+ mBatteryHealthy = !batteryService.isBatteryLow();
+ mCharging = batteryService.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ } else {
+ // Unavailable for some reason, we default to false and let ACTION_BATTERY_[OK,LOW]
+ // sort it out.
+ }
+ }
+
+ boolean isOnStablePower() {
+ return mCharging && mBatteryHealthy;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onReceiveInternal(intent);
+ }
+
+ @VisibleForTesting
+ public void onReceiveInternal(Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_BATTERY_LOW.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Battery life too low to do work. @ "
+ + SystemClock.elapsedRealtime());
+ }
+ // If we get this action, the battery is discharging => it isn't plugged in so
+ // there's no work to cancel. We track this variable for the case where it is
+ // charging, but hasn't been for long enough to be healthy.
+ mBatteryHealthy = false;
+ } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Battery life healthy enough to do work. @ "
+ + SystemClock.elapsedRealtime());
+ }
+ mBatteryHealthy = true;
+ maybeReportNewChargingState();
+ } else if (Intent.ACTION_POWER_CONNECTED.equals(action)) {
+ // Set up an alarm for ACTION_CHARGING_STABLE - we don't want to kick off tasks
+ // here if the user unplugs the phone immediately.
+ mAlarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + STABLE_CHARGING_THRESHOLD_MILLIS,
+ mStableChargingTriggerIntent);
+ mCharging = true;
+ } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
+ // If an alarm is set, breathe a sigh of relief and cancel it - crisis averted.
+ mAlarm.cancel(mStableChargingTriggerIntent);
+ mCharging = false;
+ maybeReportNewChargingState();
+ }else if (ACTION_CHARGING_STABLE.equals(action)) {
+ // Here's where we actually do the notify for a task being ready.
+ if (DEBUG) {
+ Slog.d(TAG, "Battery connected fired @ " + SystemClock.elapsedRealtime());
+ }
+ if (mCharging) { // Should never receive this intent if mCharging is false.
+ maybeReportNewChargingState();
+ }
+ }
+ }
+ }
+}
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 474af8f..4819460 100644
--- a/services/core/java/com/android/server/task/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/task/controllers/ConnectivityController.java
@@ -27,6 +27,7 @@ import android.os.UserHandle;
import android.util.Log;
import android.util.Slog;
+import com.android.server.task.StateChangedListener;
import com.android.server.task.TaskManagerService;
import java.util.LinkedList;
@@ -39,7 +40,6 @@ import java.util.List;
*/
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 =
@@ -54,13 +54,13 @@ public class ConnectivityController extends StateController {
public static synchronized ConnectivityController get(TaskManagerService taskManager) {
if (mSingleton == null) {
- mSingleton = new ConnectivityController(taskManager);
+ mSingleton = new ConnectivityController(taskManager, taskManager.getContext());
}
return mSingleton;
}
- private ConnectivityController(TaskManagerService service) {
- super(service);
+ private ConnectivityController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
// Register connectivity changed BR.
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_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 9489644..c47faca 100644
--- a/services/core/java/com/android/server/task/controllers/IdleController.java
+++ b/services/core/java/com/android/server/task/controllers/IdleController.java
@@ -28,11 +28,11 @@ import android.content.IntentFilter;
import android.os.SystemClock;
import android.util.Slog;
+import com.android.server.task.StateChangedListener;
import com.android.server.task.TaskManagerService;
public class IdleController extends StateController {
private static final String TAG = "IdleController";
- private static final boolean DEBUG = false;
// Policy: we decide that we're "idle" if the device has been unused /
// screen off or dreaming for at least this long
@@ -52,14 +52,14 @@ public class IdleController extends StateController {
public static IdleController get(TaskManagerService service) {
synchronized (sCreationLock) {
if (sController == null) {
- sController = new IdleController(service);
+ sController = new IdleController(service, service.getContext());
}
return sController;
}
}
- private IdleController(TaskManagerService service) {
- super(service);
+ private IdleController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
initIdleStateTracking();
}
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 ed31eac..cbe6ff8 100644
--- a/services/core/java/com/android/server/task/controllers/StateController.java
+++ b/services/core/java/com/android/server/task/controllers/StateController.java
@@ -27,13 +27,13 @@ import com.android.server.task.TaskManagerService;
* are ready to run, or whether they must be stopped.
*/
public abstract class StateController {
-
+ protected static final boolean DEBUG = true;
protected Context mContext;
protected StateChangedListener mStateChangedListener;
- public StateController(TaskManagerService service) {
- mStateChangedListener = service;
- mContext = service.getContext();
+ public StateController(StateChangedListener stateChangedListener, Context context) {
+ mStateChangedListener = stateChangedListener;
+ mContext = context;
}
/**
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 b7f84ec..33670a1 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,7 @@ package com.android.server.task.controllers;
import android.app.task.Task;
import android.content.ComponentName;
-import android.os.Bundle;
+import android.os.PersistableBundle;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -37,6 +37,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @hide
*/
public class TaskStatus {
+ public static final long DEFAULT_LATEST_RUNTIME = Long.MAX_VALUE;
+ public static final long DEFAULT_EARLIEST_RUNTIME = 0L;
+
final Task task;
final int uId;
@@ -61,7 +64,7 @@ public class TaskStatus {
* indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}.
*/
private long latestRunTimeElapsedMillis;
-
+ /** How many times this task has failed, used to compute back-off. */
private final int numFailures;
/** Provide a handle to the service that this task will be run on. */
@@ -69,36 +72,52 @@ public class TaskStatus {
return uId;
}
- /** Create a newly scheduled task. */
- public TaskStatus(Task task, int uId, boolean persisted) {
+ private TaskStatus(Task task, int uId, boolean persisted, int numFailures) {
this.task = task;
this.uId = uId;
- this.numFailures = 0;
+ this.numFailures = numFailures;
this.persisted = persisted;
+ }
+
+ /** Create a newly scheduled task. */
+ public TaskStatus(Task task, int uId, boolean persisted) {
+ this(task, uId, persisted, 0);
final long elapsedNow = SystemClock.elapsedRealtime();
- // Timing constraints
+
if (task.isPeriodic()) {
earliestRunTimeElapsedMillis = elapsedNow;
latestRunTimeElapsedMillis = elapsedNow + task.getIntervalMillis();
} else {
earliestRunTimeElapsedMillis = task.hasEarlyConstraint() ?
- elapsedNow + task.getMinLatencyMillis() : 0L;
+ elapsedNow + task.getMinLatencyMillis() : DEFAULT_EARLIEST_RUNTIME;
latestRunTimeElapsedMillis = task.hasLateConstraint() ?
- elapsedNow + task.getMaxExecutionDelayMillis() : Long.MAX_VALUE;
+ elapsedNow + task.getMaxExecutionDelayMillis() : DEFAULT_LATEST_RUNTIME;
}
}
- public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsed,
- long newLatestRuntimeElapsed, int backoffAttempt) {
- this.task = rescheduling.task;
+ /**
+ * Create a new TaskStatus that was loaded from disk. We ignore the provided
+ * {@link android.app.task.Task} time criteria because we can load a persisted periodic task
+ * from the {@link com.android.server.task.TaskStore} and still want to respect its
+ * wallclock runtime rather than resetting it on every boot.
+ * We consider a freshly loaded task to no longer be in back-off.
+ */
+ public TaskStatus(Task task, int uId, long earliestRunTimeElapsedMillis,
+ long latestRunTimeElapsedMillis) {
+ this(task, uId, true, 0);
+
+ this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
+ this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
+ }
- this.uId = rescheduling.getUid();
- this.persisted = rescheduling.isPersisted();
- this.numFailures = backoffAttempt;
+ /** Create a new task to be rescheduled with the provided parameters. */
+ public TaskStatus(TaskStatus rescheduling, long newEarliestRuntimeElapsedMillis,
+ long newLatestRuntimeElapsedMillis, int backoffAttempt) {
+ this(rescheduling.task, rescheduling.getUid(), rescheduling.isPersisted(), backoffAttempt);
- earliestRunTimeElapsedMillis = newEarliestRuntimeElapsed;
- latestRunTimeElapsedMillis = newLatestRuntimeElapsed;
+ earliestRunTimeElapsedMillis = newEarliestRuntimeElapsedMillis;
+ latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis;
}
public Task getTask() {
@@ -125,7 +144,7 @@ public class TaskStatus {
return uId;
}
- public Bundle getExtras() {
+ public PersistableBundle getExtras() {
return task.getExtras();
}
@@ -142,11 +161,11 @@ public class TaskStatus {
}
public boolean hasTimingDelayConstraint() {
- return earliestRunTimeElapsedMillis != 0L;
+ return earliestRunTimeElapsedMillis != DEFAULT_EARLIEST_RUNTIME;
}
public boolean hasDeadlineConstraint() {
- return latestRunTimeElapsedMillis != Long.MAX_VALUE;
+ return latestRunTimeElapsedMillis != DEFAULT_LATEST_RUNTIME;
}
public boolean hasIdleConstraint() {
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 72f312c..8c6dd27 100644
--- a/services/core/java/com/android/server/task/controllers/TimeController.java
+++ b/services/core/java/com/android/server/task/controllers/TimeController.java
@@ -24,6 +24,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.SystemClock;
+import com.android.server.task.StateChangedListener;
import com.android.server.task.TaskManagerService;
import java.util.Iterator;
@@ -58,13 +59,13 @@ public class TimeController extends StateController {
public static synchronized TimeController get(TaskManagerService taskManager) {
if (mSingleton == null) {
- mSingleton = new TimeController(taskManager);
+ mSingleton = new TimeController(taskManager, taskManager.getContext());
}
return mSingleton;
}
- private TimeController(TaskManagerService service) {
- super(service);
+ private TimeController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
mTaskExpiredAlarmIntent =
PendingIntent.getBroadcast(mContext, 0 /* ignored */,
new Intent(ACTION_TASK_EXPIRED), 0);
diff --git a/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java b/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java
new file mode 100644
index 0000000..e7f9ca0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java
@@ -0,0 +1,199 @@
+package com.android.server.task;
+
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.app.task.Task;
+import android.app.task.Task.Builder;
+import android.os.PersistableBundle;
+import android.test.AndroidTestCase;
+import android.test.RenamingDelegatingContext;
+import android.util.Log;
+
+import com.android.server.task.controllers.TaskStatus;
+
+import java.util.List;
+
+import static com.android.server.task.TaskStore.initAndGet;
+/**
+ * Test reading and writing correctly from file.
+ */
+public class TaskStoreTest extends AndroidTestCase {
+ private static final String TAG = "TaskStoreTest";
+ private static final String TEST_PREFIX = "_test_";
+ // private static final int USER_NON_0 = 3;
+ private static final int SOME_UID = 34234;
+ private ComponentName mComponent;
+ private static final long IO_WAIT = 600L;
+
+ TaskStore mTaskStoreUnderTest;
+ Context mTestContext;
+ TaskMapReadFinishedListener mTaskMapReadFinishedListenerStub =
+ new TaskMapReadFinishedListener() {
+ @Override
+ public void onTaskMapReadFinished(List<TaskStatus> tasks) {
+ // do nothing.
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ mTestContext = new RenamingDelegatingContext(getContext(), TEST_PREFIX);
+ Log.d(TAG, "Saving tasks to '" + mTestContext.getFilesDir() + "'");
+ mTaskStoreUnderTest = TaskStore.initAndGetForTesting(mTestContext,
+ mTestContext.getFilesDir(), mTaskMapReadFinishedListenerStub);
+ mComponent = new ComponentName(getContext().getPackageName(), StubClass.class.getName());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mTaskStoreUnderTest.clear();
+ }
+
+ public void testMaybeWriteStatusToDisk() throws Exception {
+ int taskId = 5;
+ long runByMillis = 20000L; // 20s
+ long runFromMillis = 2000L; // 2s
+ long initialBackoff = 10000L; // 10s
+
+ final Task task = new Builder(taskId, mComponent)
+ .setRequiresCharging(true)
+ .setRequiredNetworkCapabilities(Task.NetworkType.ANY)
+ .setBackoffCriteria(initialBackoff, Task.BackoffPolicy.EXPONENTIAL)
+ .setOverrideDeadline(runByMillis)
+ .setMinimumLatency(runFromMillis)
+ .build();
+ final TaskStatus ts = new TaskStatus(task, SOME_UID, true /* persisted */);
+ mTaskStoreUnderTest.add(ts);
+ Thread.sleep(IO_WAIT);
+ // Manually load tasks from xml file.
+ mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() {
+ @Override
+ public void onTaskMapReadFinished(List<TaskStatus> tasks) {
+ assertEquals("Didn't get expected number of persisted tasks.", 1, tasks.size());
+ TaskStatus loadedTaskStatus = tasks.get(0);
+ assertTasksEqual(task, loadedTaskStatus.getTask());
+ assertEquals("Different uids.", SOME_UID, tasks.get(0).getUid());
+ compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
+ ts.getEarliestRunTime(), loadedTaskStatus.getEarliestRunTime());
+ compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
+ ts.getLatestRunTimeElapsed(), loadedTaskStatus.getLatestRunTimeElapsed());
+ }
+ });
+
+ }
+
+ public void testWritingTwoFilesToDisk() throws Exception {
+ final Task task1 = new Builder(8, mComponent)
+ .setRequiresDeviceIdle(true)
+ .setPeriodic(10000L)
+ .setRequiresCharging(true)
+ .build();
+ final Task task2 = new Builder(12, mComponent)
+ .setMinimumLatency(5000L)
+ .setBackoffCriteria(15000L, Task.BackoffPolicy.LINEAR)
+ .setOverrideDeadline(30000L)
+ .setRequiredNetworkCapabilities(Task.NetworkType.UNMETERED)
+ .build();
+ final TaskStatus taskStatus1 = new TaskStatus(task1, SOME_UID, true /* persisted */);
+ final TaskStatus taskStatus2 = new TaskStatus(task2, SOME_UID, true /* persisted */);
+ mTaskStoreUnderTest.add(taskStatus1);
+ mTaskStoreUnderTest.add(taskStatus2);
+ Thread.sleep(IO_WAIT);
+ mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() {
+ @Override
+ public void onTaskMapReadFinished(List<TaskStatus> tasks) {
+ assertEquals("Incorrect # of persisted tasks.", 2, tasks.size());
+ TaskStatus loaded1 = tasks.get(0);
+ TaskStatus loaded2 = tasks.get(1);
+ assertTasksEqual(task1, loaded1.getTask());
+ assertTasksEqual(task2, loaded2.getTask());
+
+ // Check that the loaded task has the correct runtimes.
+ compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
+ taskStatus1.getEarliestRunTime(), loaded1.getEarliestRunTime());
+ compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
+ taskStatus1.getLatestRunTimeElapsed(), loaded1.getLatestRunTimeElapsed());
+ compareTimestampsSubjectToIoLatency("Early run-times not the same after read.",
+ taskStatus2.getEarliestRunTime(), loaded2.getEarliestRunTime());
+ compareTimestampsSubjectToIoLatency("Late run-times not the same after read.",
+ taskStatus2.getLatestRunTimeElapsed(), loaded2.getLatestRunTimeElapsed());
+ }
+ });
+
+ }
+
+ public void testWritingTaskWithExtras() throws Exception {
+ Task.Builder b = new Builder(8, mComponent)
+ .setRequiresDeviceIdle(true)
+ .setPeriodic(10000L)
+ .setRequiresCharging(true);
+
+ PersistableBundle extras = new PersistableBundle();
+ extras.putDouble("hello", 3.2);
+ extras.putString("hi", "there");
+ extras.putInt("into", 3);
+ b.setExtras(extras);
+ final Task task = b.build();
+ TaskStatus taskStatus = new TaskStatus(task, SOME_UID, true /* persisted */);
+
+ mTaskStoreUnderTest.add(taskStatus);
+ Thread.sleep(IO_WAIT);
+ mTaskStoreUnderTest.readTaskMapFromDisk(new TaskMapReadFinishedListener() {
+ @Override
+ public void onTaskMapReadFinished(List<TaskStatus> tasks) {
+ assertEquals("Incorrect # of persisted tasks.", 1, tasks.size());
+ TaskStatus loaded = tasks.get(0);
+ assertTasksEqual(task, loaded.getTask());
+ }
+ });
+
+ }
+
+ /**
+ * Helper function to throw an error if the provided task and TaskStatus objects are not equal.
+ */
+ private void assertTasksEqual(Task first, Task second) {
+ assertEquals("Different task ids.", first.getId(), second.getId());
+ assertEquals("Different components.", first.getService(), second.getService());
+ assertEquals("Different periodic status.", first.isPeriodic(), second.isPeriodic());
+ assertEquals("Different period.", first.getIntervalMillis(), second.getIntervalMillis());
+ assertEquals("Different inital backoff.", first.getInitialBackoffMillis(),
+ second.getInitialBackoffMillis());
+ assertEquals("Different backoff policy.", first.getBackoffPolicy(),
+ second.getBackoffPolicy());
+
+ assertEquals("Invalid charging constraint.", first.isRequireCharging(),
+ second.isRequireCharging());
+ assertEquals("Invalid idle constraint.", first.isRequireDeviceIdle(),
+ second.isRequireDeviceIdle());
+ assertEquals("Invalid unmetered constraint.",
+ first.getNetworkCapabilities() == Task.NetworkType.UNMETERED,
+ second.getNetworkCapabilities() == Task.NetworkType.UNMETERED);
+ assertEquals("Invalid connectivity constraint.",
+ first.getNetworkCapabilities() == Task.NetworkType.ANY,
+ second.getNetworkCapabilities() == Task.NetworkType.ANY);
+ assertEquals("Invalid deadline constraint.",
+ first.hasLateConstraint(),
+ second.hasLateConstraint());
+ assertEquals("Invalid delay constraint.",
+ first.hasEarlyConstraint(),
+ second.hasEarlyConstraint());
+ assertEquals("Extras don't match",
+ first.getExtras().toString(), second.getExtras().toString());
+ }
+
+ /**
+ * When comparing timestamps before and after DB read/writes (to make sure we're saving/loading
+ * the correct values), there is some latency involved that terrorises a naive assertEquals().
+ * We define a <code>DELTA_MILLIS</code> as a function variable here to make this comparision
+ * more reasonable.
+ */
+ private void compareTimestampsSubjectToIoLatency(String error, long ts1, long ts2) {
+ final long DELTA_MILLIS = 700L; // We allow up to 700ms of latency for IO read/writes.
+ assertTrue(error, Math.abs(ts1 - ts2) < DELTA_MILLIS + IO_WAIT);
+ }
+
+ private static class StubClass {}
+
+} \ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java b/services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java
new file mode 100644
index 0000000..e617caf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.controllers;
+
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.test.AndroidTestCase;
+
+import com.android.server.task.StateChangedListener;
+
+import static com.android.server.task.controllers.BatteryController.getForTesting;
+
+import static org.mockito.Mockito.*;
+
+/**
+ *
+ */
+public class BatteryControllerTest extends AndroidTestCase {
+ BatteryController mBatteryControllerUnderTest;
+
+ StateChangedListener mStateChangedListenerStub = new StateChangedListener() {
+ @Override
+ public void onControllerStateChanged() {
+
+ }
+
+ @Override
+ public void onTaskDeadlineExpired(TaskStatus taskStatus) {
+
+ }
+ };
+ BatteryController.ChargingTracker mTrackerUnderTest;
+
+ public void setUp() throws Exception {
+ mBatteryControllerUnderTest = getForTesting(mStateChangedListenerStub, getTestContext());
+ mTrackerUnderTest = mBatteryControllerUnderTest.getTracker();
+ }
+
+ public void testSendBatteryChargingIntent() throws Exception {
+ Intent batteryConnectedIntent = new Intent(Intent.ACTION_POWER_CONNECTED)
+ .setComponent(new ComponentName(getContext(), mTrackerUnderTest.getClass()));
+ Intent batteryHealthyIntent = new Intent(Intent.ACTION_BATTERY_OKAY)
+ .setComponent(new ComponentName(getContext(), mTrackerUnderTest.getClass()));
+
+ mTrackerUnderTest.onReceiveInternal(batteryConnectedIntent);
+ mTrackerUnderTest.onReceiveInternal(batteryHealthyIntent);
+
+ assertTrue(mTrackerUnderTest.isOnStablePower());
+ }
+
+}