From 3d86fd2bb9db6067c49634bc4c6cdb4d5235ad36 Mon Sep 17 00:00:00 2001 From: Matthew Williams Date: Fri, 16 May 2014 18:02:17 -0700 Subject: Add persistence of tasks for TaskManager & BatteryController Tasks are persisted only if the client has the RECEIVE_BOOT_COMPLETED permission. This applies to both periodic and one-off tasks. Write out task as xml, using PersistableBundle for the extras data. Todo: Add persistable bundle to xml when ag/468207 is merged. Also added BatteryController logic. Change-Id: I23eeeb8b3bc6ba155e7fa4ec25857a68ee8b1567 --- api/current.txt | 6 +- core/java/android/app/task/Task.java | 46 +- core/java/android/app/task/TaskParams.java | 14 +- .../android/server/task/TaskManagerService.java | 56 +- .../java/com/android/server/task/TaskStore.java | 574 +++++++++++++++++++-- .../server/task/controllers/BatteryController.java | 213 ++++++++ .../task/controllers/ConnectivityController.java | 8 +- .../server/task/controllers/IdleController.java | 8 +- .../server/task/controllers/StateController.java | 8 +- .../server/task/controllers/TaskStatus.java | 57 +- .../server/task/controllers/TimeController.java | 7 +- .../src/com/android/server/task/TaskStoreTest.java | 199 +++++++ .../task/controllers/BatteryControllerTest.java | 66 +++ 13 files changed, 1138 insertions(+), 124 deletions(-) create mode 100644 services/core/java/com/android/server/task/controllers/BatteryController.java create mode 100644 services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java create mode 100644 services/tests/servicestests/src/com/android/server/task/controllers/BatteryControllerTest.java diff --git a/api/current.txt b/api/current.txt index 79fb8df..af39259 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5362,7 +5362,7 @@ package android.app.task { public class Task implements android.os.Parcelable { method public int describeContents(); method public int getBackoffPolicy(); - method public android.os.Bundle getExtras(); + method public android.os.PersistableBundle getExtras(); method public int getId(); method public long getInitialBackoffMillis(); method public long getIntervalMillis(); @@ -5386,7 +5386,7 @@ package android.app.task { ctor public Task.Builder(int, android.content.ComponentName); method public android.app.task.Task build(); method public android.app.task.Task.Builder setBackoffCriteria(long, int); - method public android.app.task.Task.Builder setExtras(android.os.Bundle); + method public android.app.task.Task.Builder setExtras(android.os.PersistableBundle); method public android.app.task.Task.Builder setMinimumLatency(long); method public android.app.task.Task.Builder setOverrideDeadline(long); method public android.app.task.Task.Builder setPeriodic(long); @@ -5413,7 +5413,7 @@ package android.app.task { public class TaskParams implements android.os.Parcelable { method public int describeContents(); - method public android.os.Bundle getExtras(); + method public android.os.PersistableBundle getExtras(); method public int getTaskId(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; diff --git a/core/java/android/app/task/Task.java b/core/java/android/app/task/Task.java index ca4aeb2..87d57fb 100644 --- a/core/java/android/app/task/Task.java +++ b/core/java/android/app/task/Task.java @@ -20,6 +20,7 @@ import android.content.ComponentName; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; /** * Container of data passed to the {@link android.app.task.TaskManager} fully encapsulating the @@ -37,6 +38,18 @@ public class Task implements Parcelable { } /** + * Amount of backoff a task has initially by default, in milliseconds. + * @hide. + */ + public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 5000L; + + /** + * Default type of backoff. + * @hide + */ + public static final int DEFAULT_BACKOFF_POLICY = BackoffPolicy.EXPONENTIAL; + + /** * Linear: retry_time(failure_time, t) = failure_time + initial_retry_delay * t, t >= 1 * Expon: retry_time(failure_time, t) = failure_time + initial_retry_delay ^ t, t >= 1 */ @@ -47,7 +60,7 @@ public class Task implements Parcelable { private final int taskId; // TODO: Change this to use PersistableBundle when that lands in master. - private final Bundle extras; + private final PersistableBundle extras; private final ComponentName service; private final boolean requireCharging; private final boolean requireDeviceIdle; @@ -71,7 +84,7 @@ public class Task implements Parcelable { /** * Bundle of extras which are returned to your application at execution time. */ - public Bundle getExtras() { + public PersistableBundle getExtras() { return extras; } @@ -171,7 +184,7 @@ public class Task implements Parcelable { private Task(Parcel in) { taskId = in.readInt(); - extras = in.readBundle(); + extras = in.readPersistableBundle(); service = ComponentName.readFromParcel(in); requireCharging = in.readInt() == 1; requireDeviceIdle = in.readInt() == 1; @@ -188,7 +201,7 @@ public class Task implements Parcelable { private Task(Task.Builder b) { taskId = b.mTaskId; - extras = new Bundle(b.mExtras); + extras = new PersistableBundle(b.mExtras); service = b.mTaskService; requireCharging = b.mRequiresCharging; requireDeviceIdle = b.mRequiresDeviceIdle; @@ -211,7 +224,7 @@ public class Task implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(taskId); - out.writeBundle(extras); + out.writePersistableBundle(extras); ComponentName.writeToParcel(service, out); out.writeInt(requireCharging ? 1 : 0); out.writeInt(requireDeviceIdle ? 1 : 0); @@ -238,12 +251,10 @@ public class Task implements Parcelable { } }; - /** - * Builder class for constructing {@link Task} objects. - */ + /** Builder class for constructing {@link Task} objects. */ public static final class Builder { private int mTaskId; - private Bundle mExtras; + private PersistableBundle mExtras = PersistableBundle.EMPTY; private ComponentName mTaskService; // Requirements. private boolean mRequiresCharging; @@ -258,8 +269,8 @@ public class Task implements Parcelable { private boolean mHasLateConstraint; private long mIntervalMillis; // Back-off parameters. - private long mInitialBackoffMillis = 5000L; - private int mBackoffPolicy = BackoffPolicy.EXPONENTIAL; + private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS; + private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY; /** Easy way to track whether the client has tried to set a back-off policy. */ private boolean mBackoffPolicySet = false; @@ -279,7 +290,7 @@ public class Task implements Parcelable { * Set optional extras. This is persisted, so we only allow primitive types. * @param extras Bundle containing extras you want the scheduler to hold on to for you. */ - public Builder setExtras(Bundle extras) { + public Builder setExtras(PersistableBundle extras) { mExtras = extras; return this; } @@ -394,18 +405,13 @@ public class Task implements Parcelable { * @return The task object to hand to the TaskManager. This object is immutable. */ public Task build() { - if (mExtras == null) { - mExtras = Bundle.EMPTY; - } - if (mTaskId < 0) { - throw new IllegalArgumentException("Task id must be greater than 0."); - } + mExtras = new PersistableBundle(mExtras); // Make our own copy. // Check that a deadline was not set on a periodic task. - if (mIsPeriodic && mHasLateConstraint) { + if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) { throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + "periodic task."); } - if (mIsPeriodic && mHasEarlyConstraint) { + if (mIsPeriodic && (mMinLatencyMillis != 0L)) { throw new IllegalArgumentException("Can't call setMinimumLatency() on a " + "periodic task"); } diff --git a/core/java/android/app/task/TaskParams.java b/core/java/android/app/task/TaskParams.java index dacb348..f4908c6 100644 --- a/core/java/android/app/task/TaskParams.java +++ b/core/java/android/app/task/TaskParams.java @@ -16,10 +16,10 @@ package android.app.task; -import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; /** * Contains the parameters used to configure/identify your task. You do not create this object @@ -28,11 +28,11 @@ import android.os.Parcelable; public class TaskParams implements Parcelable { private final int taskId; - private final Bundle extras; + private final PersistableBundle extras; private final IBinder callback; /** @hide */ - public TaskParams(int taskId, Bundle extras, IBinder callback) { + public TaskParams(int taskId, PersistableBundle extras, IBinder callback) { this.taskId = taskId; this.extras = extras; this.callback = callback; @@ -47,10 +47,10 @@ public class TaskParams implements Parcelable { /** * @return The extras you passed in when constructing this task with - * {@link android.app.task.Task.Builder#setExtras(android.os.Bundle)}. This will + * {@link android.app.task.Task.Builder#setExtras(android.os.PersistableBundle)}. This will * never be null. If you did not set any extras this will be an empty bundle. */ - public Bundle getExtras() { + public PersistableBundle getExtras() { return extras; } @@ -61,7 +61,7 @@ public class TaskParams implements Parcelable { private TaskParams(Parcel in) { taskId = in.readInt(); - extras = in.readBundle(); + extras = in.readPersistableBundle(); callback = in.readStrongBinder(); } @@ -73,7 +73,7 @@ public class TaskParams implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(taskId); - dest.writeBundle(extras); + dest.writePersistableBundle(extras); dest.writeStrongBinder(callback); } 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 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(); 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 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 not thread-safe. + * - This class is not thread-safe. + * + * Note on locking: + * All callers to this class must lock on the class object they are calling. + * 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 mTasks; + final ArraySet 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(); + + 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 it = mTasks.iterator(); - boolean removed = false; + Iterator 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 it = mTasks.iterator(); + boolean changed = false; + Iterator 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 getTasks() { - return mTasks; + public ArraySet 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 intialiseTasksFromDisk() { - return new ArraySet(); + private class ReadTaskMapFromDiskRunnable implements Runnable { + private TaskMapReadFinishedListener mCallback; + public ReadTaskMapFromDiskRunnable(TaskMapReadFinishedListener callback) { + mCallback = callback; + } + + @Override + public void run() { + try { + List 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 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 tasks = new ArrayList(); + // 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 + 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 "" 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 start tag. + return null; + } + try { + buildConstraintsFromXml(taskBuilder, parser); + } catch (NumberFormatException e) { + Slog.d(TAG, "Error reading constraints, skipping."); + return null; + } + parser.next(); // Consume + + // Read out execution parameters tag. + do { + eventType = parser.next(); + } while (eventType == XmlPullParser.TEXT); + if (eventType != XmlPullParser.START_TAG) { + return null; + } + + Pair 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 + + return new TaskStatus(taskBuilder.build(), uid, runtimes.first, runtimes.second); + } + + private Task.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException { + // Pull out required fields from 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 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 mTrackedTasks = new ArrayList(); + 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 mTrackedTasks = new LinkedList(); 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 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 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 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 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 DELTA_MILLIS 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()); + } + +} -- cgit v1.1