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
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 @@
   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 @@
     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 @@
 
   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..87d57fb4 100644
--- a/core/java/android/app/task/Task.java
+++ b/core/java/android/app/task/Task.java
@@ -20,6 +20,7 @@
 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 @@
     }
 
     /**
+     * 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 @@
 
     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 @@
     /**
      * Bundle of extras which are returned to your application at execution time.
      */
-    public Bundle getExtras() {
+    public PersistableBundle getExtras() {
         return extras;
     }
 
@@ -171,7 +184,7 @@
 
     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 @@
 
     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 @@
     @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 @@
         }
     };
 
-    /**
-     * 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 @@
         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 @@
          * 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 @@
          * @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 dacb3480..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 @@
 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 @@
 
     /**
      * @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 @@
 
     private TaskParams(Parcel in) {
         taskId = in.readInt();
-        extras = in.readBundle();
+        extras = in.readPersistableBundle();
         callback = in.readStrongBinder();
     }
 
@@ -73,7 +73,7 @@
     @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.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 @@
  * 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 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 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 @@
             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 @@
      * {@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 @@
         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 @@
  *     - 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 @@
      * 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 @@
      * @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.");
             }
-            mDirtyOperations = 0;
+            mIoHandler.post(new WriteTasksMapToDiskRunnable());
         }
-        return true;
+    }
+
+    private void readTaskMapFromDiskAsync(TaskMapReadFinishedListener callback) {
+        mIoHandler.post(new ReadTaskMapFromDiskRunnable(callback));
+    }
+
+    public void readTaskMapFromDisk(TaskMapReadFinishedListener callback) {
+        new ReadTaskMapFromDiskRunnable(callback).run();
     }
 
     /**
-     *
-     * @return
+     * Runnable that writes {@link #mTasksSet} out to xml.
+     * NOTE: This Runnable locks on TaskStore.this
      */
-    // TODO: Implement this.
-    private ArraySet<TaskStatus> intialiseTasksFromDisk() {
-        return new ArraySet<TaskStatus>();
+    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);
+            }
+        }
+    }
+
+    /**
+     * Runnable that reads list of persisted task from xml.
+     * NOTE: This Runnable locks on TaskStore.this
+     */
+    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.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 @@
  */
 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 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.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 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 @@
  * 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 @@
 
 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 @@
  * @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 @@
      * 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 @@
         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.uId = rescheduling.getUid();
-        this.persisted = rescheduling.isPersisted();
-        this.numFailures = backoffAttempt;
+        this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
+        this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
+    }
 
-        earliestRunTimeElapsedMillis = newEarliestRuntimeElapsed;
-        latestRunTimeElapsedMillis = newLatestRuntimeElapsed;
+    /** 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 = newEarliestRuntimeElapsedMillis;
+        latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis;
     }
 
     public Task getTask() {
@@ -125,7 +144,7 @@
         return uId;
     }
 
-    public Bundle getExtras() {
+    public PersistableBundle getExtras() {
         return task.getExtras();
     }
 
@@ -142,11 +161,11 @@
     }
 
     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.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 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());
+    }
+
+}