Merge "Add new "work queue" feature to JobScheduler." into oc-dev am: 0a0a63c8b1
am: cba1893400

Change-Id: I67e7438a5563192391366683695c7fc4514a5bd6
diff --git a/api/current.txt b/api/current.txt
index c52608a..2b2b608 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6830,6 +6830,8 @@
   }
 
   public class JobParameters implements android.os.Parcelable {
+    method public void completeWork(android.app.job.JobWorkItem);
+    method public android.app.job.JobWorkItem dequeueWork();
     method public int describeContents();
     method public android.content.ClipData getClipData();
     method public int getClipGrantFlags();
@@ -6847,6 +6849,7 @@
     ctor public JobScheduler();
     method public abstract void cancel(int);
     method public abstract void cancelAll();
+    method public abstract int enqueue(android.app.job.JobInfo, android.app.job.JobWorkItem);
     method public abstract java.util.List<android.app.job.JobInfo> getAllPendingJobs();
     method public abstract android.app.job.JobInfo getPendingJob(int);
     method public abstract int schedule(android.app.job.JobInfo);
@@ -6863,6 +6866,15 @@
     field public static final java.lang.String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE";
   }
 
+  public final class JobWorkItem implements android.os.Parcelable {
+    ctor public JobWorkItem(android.content.Intent);
+    ctor public JobWorkItem(android.os.Parcel);
+    method public int describeContents();
+    method public android.content.Intent getIntent();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR;
+  }
+
 }
 
 package android.app.usage {
diff --git a/api/system-current.txt b/api/system-current.txt
index 5e0f43a..a36de39 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -7260,6 +7260,8 @@
   }
 
   public class JobParameters implements android.os.Parcelable {
+    method public void completeWork(android.app.job.JobWorkItem);
+    method public android.app.job.JobWorkItem dequeueWork();
     method public int describeContents();
     method public android.content.ClipData getClipData();
     method public int getClipGrantFlags();
@@ -7277,6 +7279,7 @@
     ctor public JobScheduler();
     method public abstract void cancel(int);
     method public abstract void cancelAll();
+    method public abstract int enqueue(android.app.job.JobInfo, android.app.job.JobWorkItem);
     method public abstract java.util.List<android.app.job.JobInfo> getAllPendingJobs();
     method public abstract android.app.job.JobInfo getPendingJob(int);
     method public abstract int schedule(android.app.job.JobInfo);
@@ -7294,6 +7297,15 @@
     field public static final java.lang.String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE";
   }
 
+  public final class JobWorkItem implements android.os.Parcelable {
+    ctor public JobWorkItem(android.content.Intent);
+    ctor public JobWorkItem(android.os.Parcel);
+    method public int describeContents();
+    method public android.content.Intent getIntent();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR;
+  }
+
 }
 
 package android.app.usage {
diff --git a/api/test-current.txt b/api/test-current.txt
index 1087108..dbdaa36 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -6859,6 +6859,8 @@
   }
 
   public class JobParameters implements android.os.Parcelable {
+    method public void completeWork(android.app.job.JobWorkItem);
+    method public android.app.job.JobWorkItem dequeueWork();
     method public int describeContents();
     method public android.content.ClipData getClipData();
     method public int getClipGrantFlags();
@@ -6876,6 +6878,7 @@
     ctor public JobScheduler();
     method public abstract void cancel(int);
     method public abstract void cancelAll();
+    method public abstract int enqueue(android.app.job.JobInfo, android.app.job.JobWorkItem);
     method public abstract java.util.List<android.app.job.JobInfo> getAllPendingJobs();
     method public abstract android.app.job.JobInfo getPendingJob(int);
     method public abstract int schedule(android.app.job.JobInfo);
@@ -6892,6 +6895,15 @@
     field public static final java.lang.String PERMISSION_BIND = "android.permission.BIND_JOB_SERVICE";
   }
 
+  public final class JobWorkItem implements android.os.Parcelable {
+    ctor public JobWorkItem(android.content.Intent);
+    ctor public JobWorkItem(android.os.Parcel);
+    method public int describeContents();
+    method public android.content.Intent getIntent();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR;
+  }
+
 }
 
 package android.app.usage {
diff --git a/core/java/android/app/JobSchedulerImpl.java b/core/java/android/app/JobSchedulerImpl.java
index e30b96f..4ac44f7 100644
--- a/core/java/android/app/JobSchedulerImpl.java
+++ b/core/java/android/app/JobSchedulerImpl.java
@@ -20,6 +20,8 @@
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.app.job.IJobScheduler;
+import android.app.job.JobWorkItem;
+import android.content.Intent;
 import android.os.RemoteException;
 
 import java.util.List;
@@ -46,6 +48,15 @@
     }
 
     @Override
+    public int enqueue(JobInfo job, JobWorkItem work) {
+        try {
+            return mBinder.enqueue(job, work);
+        } catch (RemoteException e) {
+            return JobScheduler.RESULT_FAILURE;
+        }
+    }
+
+    @Override
     public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) {
         try {
             return mBinder.scheduleAsPackage(job, packageName, userId, tag);
diff --git a/core/java/android/app/job/IJobCallback.aidl b/core/java/android/app/job/IJobCallback.aidl
index 2d3948f..e7695e2 100644
--- a/core/java/android/app/job/IJobCallback.aidl
+++ b/core/java/android/app/job/IJobCallback.aidl
@@ -16,6 +16,8 @@
 
 package android.app.job;
 
+import android.app.job.JobWorkItem;
+
 /**
  * The server side of the JobScheduler IPC protocols.  The app-side implementation
  * invokes on this interface to indicate completion of the (asynchronous) instructions
@@ -43,6 +45,14 @@
      */
     void acknowledgeStopMessage(int jobId, boolean reschedule);
     /*
+     * Called to deqeue next work item for the job.
+     */
+    JobWorkItem dequeueWork(int jobId);
+    /*
+     * Called to report that job has completed processing a work item.
+     */
+    boolean completeWork(int jobId, int workId);
+    /*
      * Tell the job manager that the client is done with its execution, so that it can go on to
      * the next one and stop attributing wakelock time to us etc.
      *
diff --git a/core/java/android/app/job/IJobScheduler.aidl b/core/java/android/app/job/IJobScheduler.aidl
index b6eec27..e94da0c 100644
--- a/core/java/android/app/job/IJobScheduler.aidl
+++ b/core/java/android/app/job/IJobScheduler.aidl
@@ -17,6 +17,7 @@
 package android.app.job;
 
 import android.app.job.JobInfo;
+import android.app.job.JobWorkItem;
 
  /**
   * IPC interface that supports the app-facing {@link #JobScheduler} api.
@@ -24,6 +25,7 @@
   */
 interface IJobScheduler {
     int schedule(in JobInfo job);
+    int enqueue(in JobInfo job, in JobWorkItem work);
     int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag);
     void cancel(int jobId);
     void cancelAll();
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 8220ff9..412e445 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -23,6 +23,7 @@
 import android.content.ClipData;
 import android.content.ComponentName;
 import android.net.Uri;
+import android.os.BaseBundle;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -442,6 +443,130 @@
         return hasLateConstraint;
     }
 
+    private static boolean kindofEqualsBundle(BaseBundle a, BaseBundle b) {
+        return (a == b) || (a != null && a.kindofEquals(b));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof JobInfo)) {
+            return false;
+        }
+        JobInfo j = (JobInfo) o;
+        if (jobId != j.jobId) {
+            return false;
+        }
+        // XXX won't be correct if one is parcelled and the other not.
+        if (!kindofEqualsBundle(extras, j.extras)) {
+            return false;
+        }
+        // XXX won't be correct if one is parcelled and the other not.
+        if (!kindofEqualsBundle(transientExtras, j.transientExtras)) {
+            return false;
+        }
+        // XXX for now we consider two different clip data objects to be different,
+        // regardless of whether their contents are the same.
+        if (clipData != j.clipData) {
+            return false;
+        }
+        if (clipGrantFlags != j.clipGrantFlags) {
+            return false;
+        }
+        if (!Objects.equals(service, j.service)) {
+            return false;
+        }
+        if (constraintFlags != j.constraintFlags) {
+            return false;
+        }
+        if (!Objects.deepEquals(triggerContentUris, j.triggerContentUris)) {
+            return false;
+        }
+        if (triggerContentUpdateDelay != j.triggerContentUpdateDelay) {
+            return false;
+        }
+        if (triggerContentMaxDelay != j.triggerContentMaxDelay) {
+            return false;
+        }
+        if (hasEarlyConstraint != j.hasEarlyConstraint) {
+            return false;
+        }
+        if (hasLateConstraint != j.hasLateConstraint) {
+            return false;
+        }
+        if (networkType != j.networkType) {
+            return false;
+        }
+        if (minLatencyMillis != j.minLatencyMillis) {
+            return false;
+        }
+        if (maxExecutionDelayMillis != j.maxExecutionDelayMillis) {
+            return false;
+        }
+        if (isPeriodic != j.isPeriodic) {
+            return false;
+        }
+        if (isPersisted != j.isPersisted) {
+            return false;
+        }
+        if (intervalMillis != j.intervalMillis) {
+            return false;
+        }
+        if (flexMillis != j.flexMillis) {
+            return false;
+        }
+        if (initialBackoffMillis != j.initialBackoffMillis) {
+            return false;
+        }
+        if (backoffPolicy != j.backoffPolicy) {
+            return false;
+        }
+        if (priority != j.priority) {
+            return false;
+        }
+        if (flags != j.flags) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = jobId;
+        if (extras != null) {
+            hashCode = 31*hashCode + extras.hashCode();
+        }
+        if (transientExtras != null) {
+            hashCode = 31*hashCode + transientExtras.hashCode();
+        }
+        if (clipData != null) {
+            hashCode = 31*hashCode + clipData.hashCode();
+        }
+        hashCode = 31*hashCode + clipGrantFlags;
+        if (service != null) {
+            hashCode = 31*hashCode + service.hashCode();
+        }
+        hashCode = 31*hashCode + constraintFlags;
+        if (triggerContentUris != null) {
+            hashCode = 31*hashCode + triggerContentUris.hashCode();
+        }
+        hashCode = 31*hashCode + Long.hashCode(triggerContentUpdateDelay);
+        hashCode = 31*hashCode + Long.hashCode(triggerContentMaxDelay);
+        hashCode = 31*hashCode + Boolean.hashCode(hasEarlyConstraint);
+        hashCode = 31*hashCode + Boolean.hashCode(hasLateConstraint);
+        hashCode = 31*hashCode + networkType;
+        hashCode = 31*hashCode + Long.hashCode(minLatencyMillis);
+        hashCode = 31*hashCode + Long.hashCode(maxExecutionDelayMillis);
+        hashCode = 31*hashCode + Boolean.hashCode(isPeriodic);
+        hashCode = 31*hashCode + Boolean.hashCode(isPersisted);
+        hashCode = 31*hashCode + Long.hashCode(intervalMillis);
+        hashCode = 31*hashCode + Long.hashCode(flexMillis);
+        hashCode = 31*hashCode + Long.hashCode(initialBackoffMillis);
+        hashCode = 31*hashCode + backoffPolicy;
+        hashCode = 31*hashCode + priority;
+        hashCode = 31*hashCode + flags;
+        return hashCode;
+    }
+
     private JobInfo(Parcel in) {
         jobId = in.readInt();
         extras = in.readPersistableBundle();
diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java
index 8d52d3b..016a0fa 100644
--- a/core/java/android/app/job/JobParameters.java
+++ b/core/java/android/app/job/JobParameters.java
@@ -24,6 +24,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
+import android.os.RemoteException;
 
 /**
  * Contains the parameters used to configure/identify your job. You do not create this object
@@ -155,6 +156,53 @@
         return mTriggeredContentAuthorities;
     }
 
+    /**
+     * Dequeue the next pending {@link JobWorkItem} from these JobParameters associated with their
+     * currently running job.  Calling this method when there is no more work available and all
+     * previously dequeued work has been completed will result in the system taking care of
+     * stopping the job for you --
+     * you should not call {@link JobService#jobFinished(JobParameters, boolean)} yourself
+     * (otherwise you risk losing an upcoming JobWorkItem that is being enqueued at the same time).
+     *
+     * @return Returns a new {@link JobWorkItem} if there is one pending, otherwise null.
+     * If null is returned, the system will also stop the job if all work has also been completed.
+     * (This means that for correct operation, you must always call dequeueWork() after you have
+     * completed other work, to check either for more work or allow the system to stop the job.)
+     */
+    public JobWorkItem dequeueWork() {
+        try {
+            return getCallback().dequeueWork(getJobId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report the completion of executing a {@link JobWorkItem} previously returned by
+     * {@link #dequeueWork()}.  This tells the system you are done with the
+     * work associated with that item, so it will not be returned again.  Note that if this
+     * is the last work in the queue, completing it here will <em>not</em> finish the overall
+     * job -- for that to happen, you still need to call {@link #dequeueWork()}
+     * again.
+     *
+     * <p>If you are enqueueing work into a job, you must call this method for each piece
+     * of work you process.  Do <em>not</em> call
+     * {@link JobService#jobFinished(JobParameters, boolean)}
+     * or else you can lose work in your queue.</p>
+     *
+     * @param work The work you have completed processing, as previously returned by
+     * {@link #dequeueWork()}
+     */
+    public void completeWork(JobWorkItem work) {
+        try {
+            if (!getCallback().completeWork(getJobId(), work.getWorkId())) {
+                throw new IllegalArgumentException("Given work is not active: " + work);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /** @hide */
     public IJobCallback getCallback() {
         return IJobCallback.Stub.asInterface(callback);
diff --git a/core/java/android/app/job/JobScheduler.java b/core/java/android/app/job/JobScheduler.java
index 1b640d0..e0afe03 100644
--- a/core/java/android/app/job/JobScheduler.java
+++ b/core/java/android/app/job/JobScheduler.java
@@ -19,6 +19,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.content.ClipData;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.PersistableBundle;
 
 import java.util.List;
 
@@ -59,6 +63,10 @@
     public static final int RESULT_SUCCESS = 1;
 
     /**
+     * Schedule a job to be executed.  Will replace any currently scheduled job with the same
+     * ID with the new information in the {@link JobInfo}.  If a job with the given ID is currently
+     * running, it will be stopped.
+     *
      * @param job The job you wish scheduled. See
      * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs
      * you can schedule.
@@ -67,6 +75,42 @@
     public abstract int schedule(JobInfo job);
 
     /**
+     * Similar to {@link #schedule}, but allows you to enqueue work for an existing job.  If a job
+     * with the same ID is already scheduled, it will be replaced with the new {@link JobInfo}, but
+     * any previously enqueued work will remain and be dispatched the next time it runs.  If a job
+     * with the same ID is already running, the new work will be enqueued for it.
+     *
+     * <p>The work you enqueue is later retrieved through
+     * {@link JobParameters#dequeueWork() JobParameters.dequeueWork()}.  Be sure to see there
+     * about how to process work; the act of enqueueing work changes how you should handle the
+     * overall lifecycle of an executing job.</p>
+     *
+     * <p>It is strongly encouraged that you use the same {@link JobInfo} for all work you
+     * enqueue.  This will allow the system to optimal schedule work along with any pending
+     * and/or currently running work.  If the JobInfo changes from the last time the job was
+     * enqueued, the system will need to update the associated JobInfo, which can cause a disruption
+     * in exection.  In particular, this can result in any currently running job that is processing
+     * previous work to be stopped and restarted with the new JobInfo.</p>
+     *
+     * <p>It is recommended that you avoid using
+     * {@link JobInfo.Builder#setExtras(PersistableBundle)} or
+     * {@link JobInfo.Builder#setTransientExtras(Bundle)} with a JobInfo you are using to
+     * enqueue work.  The system will try to compare these extras with the previous JobInfo,
+     * but there are situations where it may get this wrong and count the JobInfo as changing.
+     * (That said, you should be relatively safe with a simple set of consistent data in these
+     * fields.)  You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with
+     * work you are enqueue, since currently this will always be treated as a different JobInfo,
+     * even if the ClipData contents is exactly the same.</p>
+     *
+     * @param job The job you wish to enqueue work for. See
+     * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs
+     * you can schedule.
+     * @param work New work to enqueue.  This will be available later when the job starts running.
+     * @return An int representing ({@link #RESULT_SUCCESS} or {@link #RESULT_FAILURE}).
+     */
+    public abstract int enqueue(JobInfo job, JobWorkItem work);
+
+    /**
      *
      * @param job The job to be scheduled.
      * @param packageName The package on behalf of which the job is to be scheduled. This will be
diff --git a/core/java/android/app/job/JobWorkItem.aidl b/core/java/android/app/job/JobWorkItem.aidl
new file mode 100644
index 0000000..e8fe47d
--- /dev/null
+++ b/core/java/android/app/job/JobWorkItem.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 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 android.app.job;
+
+/** @hide */
+parcelable JobWorkItem;
diff --git a/core/java/android/app/job/JobWorkItem.java b/core/java/android/app/job/JobWorkItem.java
new file mode 100644
index 0000000..4bb057e
--- /dev/null
+++ b/core/java/android/app/job/JobWorkItem.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 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 android.app.job;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A unit of work that can be enqueued for a job using
+ * {@link JobScheduler#enqueue JobScheduler.enqueue}.
+ */
+final public class JobWorkItem implements Parcelable {
+    final Intent mIntent;
+    int mWorkId;
+
+    /**
+     * Create a new piece of work.
+     * @param intent The general Intent describing this work.
+     */
+    public JobWorkItem(Intent intent) {
+        mIntent = intent;
+    }
+
+    /**
+     * Return the Intent associated with this work.
+     */
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    /**
+     * @hide
+     */
+    public void setWorkId(int id) {
+        mWorkId = id;
+    }
+
+    /**
+     * @hide
+     */
+    public int getWorkId() {
+        return mWorkId;
+    }
+
+    public String toString() {
+        return "JobWorkItem{id=" + mWorkId + " intent=" + mIntent + "}";
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel out, int flags) {
+        if (mIntent != null) {
+            out.writeInt(1);
+            mIntent.writeToParcel(out, 0);
+        } else {
+            out.writeInt(0);
+        }
+        out.writeInt(mWorkId);
+    }
+
+    public static final Parcelable.Creator<JobWorkItem> CREATOR
+            = new Parcelable.Creator<JobWorkItem>() {
+        public JobWorkItem createFromParcel(Parcel in) {
+            return new JobWorkItem(in);
+        }
+
+        public JobWorkItem[] newArray(int size) {
+            return new JobWorkItem[size];
+        }
+    };
+
+    public JobWorkItem(Parcel in) {
+        if (in.readInt() != 0) {
+            mIntent = Intent.CREATOR.createFromParcel(in);
+        } else {
+            mIntent = null;
+        }
+        mWorkId = in.readInt();
+    }
+}
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index 6f388e2..65025fb 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -325,6 +325,23 @@
     }
 
     /**
+     * @hide This kind-of does an equality comparison.  Kind-of.
+     */
+    public boolean kindofEquals(BaseBundle other) {
+        if (other == null) {
+            return false;
+        }
+        if (isParcelled() != other.isParcelled()) {
+            // Big kind-of here!
+            return false;
+        } else if (isParcelled()) {
+            return mParcelledData.compareData(other.mParcelledData) == 0;
+        } else {
+            return mMap.equals(other.mMap);
+        }
+    }
+
+    /**
      * Removes all elements from the mapping of this Bundle.
      */
     public void clear() {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 76128e6..c3836a3 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -315,6 +315,7 @@
     private static native byte[] nativeMarshall(long nativePtr);
     private static native long nativeUnmarshall(
             long nativePtr, byte[] data, int offset, int length);
+    private static native int nativeCompareData(long thisNativePtr, long otherNativePtr);
     private static native long nativeAppendFrom(
             long thisNativePtr, long otherNativePtr, int offset, int length);
     @FastNative
@@ -487,6 +488,11 @@
         updateNativeSize(nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length));
     }
 
+    /** @hide */
+    public final int compareData(Parcel other) {
+        return nativeCompareData(mNativePtr, other.mNativePtr);
+    }
+
     /**
      * Report whether the parcel contains any marshalled file descriptors.
      */
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 8f7908a..d740a76 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -617,6 +617,21 @@
     return parcel->getOpenAshmemSize();
 }
 
+static jint android_os_Parcel_compareData(JNIEnv* env, jclass clazz, jlong thisNativePtr,
+                                          jlong otherNativePtr)
+{
+    Parcel* thisParcel = reinterpret_cast<Parcel*>(thisNativePtr);
+    if (thisParcel == NULL) {
+       return 0;
+    }
+    Parcel* otherParcel = reinterpret_cast<Parcel*>(otherNativePtr);
+    if (otherParcel == NULL) {
+       return thisParcel->getOpenAshmemSize();
+    }
+
+    return thisParcel->compareData(*otherParcel);
+}
+
 static jlong android_os_Parcel_appendFrom(JNIEnv* env, jclass clazz, jlong thisNativePtr,
                                           jlong otherNativePtr, jint offset, jint length)
 {
@@ -781,6 +796,7 @@
 
     {"nativeMarshall",            "(J)[B", (void*)android_os_Parcel_marshall},
     {"nativeUnmarshall",          "(J[BII)J", (void*)android_os_Parcel_unmarshall},
+    {"nativeCompareData",         "(JJ)I", (void*)android_os_Parcel_compareData},
     {"nativeAppendFrom",          "(JJII)J", (void*)android_os_Parcel_appendFrom},
     // @FastNative
     {"nativeHasFileDescriptors",  "(J)Z", (void*)android_os_Parcel_hasFileDescriptors},
diff --git a/services/core/java/com/android/server/job/JobCompletedListener.java b/services/core/java/com/android/server/job/JobCompletedListener.java
index 655abd7..34ba753b3 100644
--- a/services/core/java/com/android/server/job/JobCompletedListener.java
+++ b/services/core/java/com/android/server/job/JobCompletedListener.java
@@ -27,5 +27,5 @@
      * Callback for when a job is completed.
      * @param needsReschedule Whether the implementing class should reschedule this job.
      */
-    void onJobCompleted(JobStatus jobStatus, boolean needsReschedule);
+    void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule);
 }
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index cd3ba4c..a0d0d77 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -37,6 +37,7 @@
 import android.app.job.JobScheduler;
 import android.app.job.JobService;
 import android.app.job.IJobScheduler;
+import android.app.job.JobWorkItem;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -582,20 +583,8 @@
         mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
     }
 
-    /**
-     * Entry point from client to schedule the provided job.
-     * This cancels the job if it's already been scheduled, and replaces it with the one provided.
-     * @param job JobInfo object containing execution parameters
-     * @param uId The package identifier of the application this job is for.
-     * @return Result of this operation. See <code>JobScheduler#RESULT_*</code> return codes.
-     */
-    public int schedule(JobInfo job, int uId) {
-        return scheduleAsPackage(job, uId, null, -1, null);
-    }
-
-    public int scheduleAsPackage(JobInfo job, int uId, String packageName, int userId,
-            String tag) {
-        JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+    public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
+            int userId, String tag) {
         try {
             if (ActivityManager.getService().isAppStartModeDisabled(uId,
                     job.getService().getPackageName())) {
@@ -605,9 +594,20 @@
             }
         } catch (RemoteException e) {
         }
-        if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString());
-        JobStatus toCancel;
         synchronized (mLock) {
+            final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
+
+            if (work != null && toCancel != null) {
+                // Fast path: we are adding work to an existing job, and the JobInfo is not
+                // changing.  We can just directly enqueue this work in to the job.
+                if (toCancel.getJob().equals(job)) {
+                    toCancel.enqueueWorkLocked(work);
+                    return JobScheduler.RESULT_SUCCESS;
+                }
+            }
+
+            JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+            if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString());
             // Jobs on behalf of others don't apply to the per-app job cap
             if (ENFORCE_MAX_JOBS && packageName == null) {
                 if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) {
@@ -618,15 +618,18 @@
             }
 
             // This may throw a SecurityException.
-            jobStatus.prepare(ActivityManager.getService());
+            jobStatus.prepareLocked(ActivityManager.getService());
 
-            toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
             if (toCancel != null) {
                 cancelJobImpl(toCancel, jobStatus);
             }
-            startTrackingJob(jobStatus, toCancel);
+            if (work != null) {
+                // If work has been supplied, enqueue it into the new job.
+                jobStatus.enqueueWorkLocked(work);
+            }
+            startTrackingJobLocked(jobStatus, toCancel);
+            mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
         }
-        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
         return JobScheduler.RESULT_SUCCESS;
     }
 
@@ -715,17 +718,17 @@
     }
 
     private void cancelJobImpl(JobStatus cancelled, JobStatus incomingJob) {
-        if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
-        cancelled.unprepare(ActivityManager.getService());
-        stopTrackingJob(cancelled, incomingJob, true /* writeBack */);
         synchronized (mLock) {
+            if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
+            cancelled.unprepareLocked(ActivityManager.getService());
+            stopTrackingJobLocked(cancelled, incomingJob, true /* writeBack */);
             // Remove from pending queue.
             if (mPendingJobs.remove(cancelled)) {
                 mJobPackageTracker.noteNonpending(cancelled);
             }
             // Cancel if running.
             stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED);
-            reportActive();
+            reportActiveLocked();
         }
     }
 
@@ -773,7 +776,7 @@
         }
     }
 
-    void reportActive() {
+    void reportActiveLocked() {
         // active is true if pending queue contains jobs OR some job is running.
         boolean active = mPendingJobs.size() > 0;
         if (mPendingJobs.size() <= 0) {
@@ -895,20 +898,18 @@
      * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
      * about.
      */
-    private void startTrackingJob(JobStatus jobStatus, JobStatus lastJob) {
-        synchronized (mLock) {
-            if (!jobStatus.isPrepared()) {
-                Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
-            }
-            final boolean update = mJobs.add(jobStatus);
-            if (mReadyToRock) {
-                for (int i = 0; i < mControllers.size(); i++) {
-                    StateController controller = mControllers.get(i);
-                    if (update) {
-                        controller.maybeStopTrackingJobLocked(jobStatus, null, true);
-                    }
-                    controller.maybeStartTrackingJobLocked(jobStatus, lastJob);
+    private void startTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+        if (!jobStatus.isPreparedLocked()) {
+            Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
+        }
+        final boolean update = mJobs.add(jobStatus);
+        if (mReadyToRock) {
+            for (int i = 0; i < mControllers.size(); i++) {
+                StateController controller = mControllers.get(i);
+                if (update) {
+                    controller.maybeStopTrackingJobLocked(jobStatus, null, true);
                 }
+                controller.maybeStartTrackingJobLocked(jobStatus, lastJob);
             }
         }
     }
@@ -917,19 +918,20 @@
      * Called when we want to remove a JobStatus object that we've finished executing. Returns the
      * object removed.
      */
-    private boolean stopTrackingJob(JobStatus jobStatus, JobStatus incomingJob,
+    private boolean stopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
             boolean writeBack) {
-        synchronized (mLock) {
-            // Remove from store as well as controllers.
-            final boolean removed = mJobs.remove(jobStatus, writeBack);
-            if (removed && mReadyToRock) {
-                for (int i=0; i<mControllers.size(); i++) {
-                    StateController controller = mControllers.get(i);
-                    controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
-                }
+        // Deal with any remaining work items in the old job.
+        jobStatus.stopTrackingJobLocked(incomingJob);
+
+        // Remove from store as well as controllers.
+        final boolean removed = mJobs.remove(jobStatus, writeBack);
+        if (removed && mReadyToRock) {
+            for (int i=0; i<mControllers.size(); i++) {
+                StateController controller = mControllers.get(i);
+                controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
             }
-            return removed;
         }
+        return removed;
     }
 
     private boolean stopJobOnServiceContextLocked(JobStatus job, int reason) {
@@ -990,7 +992,7 @@
      *
      * @see JobHandler#maybeQueueReadyJobsForExecutionLockedH
      */
-    private JobStatus getRescheduleJobForFailure(JobStatus failureToReschedule) {
+    private JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) {
         final long elapsedNowMillis = SystemClock.elapsedRealtime();
         final JobInfo job = failureToReschedule.getJob();
 
@@ -1017,7 +1019,7 @@
                 JobStatus.NO_LATEST_RUNTIME, backoffAttempts);
         for (int ic=0; ic<mControllers.size(); ic++) {
             StateController controller = mControllers.get(ic);
-            controller.rescheduleForFailure(newJob, failureToReschedule);
+            controller.rescheduleForFailureLocked(newJob, failureToReschedule);
         }
         return newJob;
     }
@@ -1065,13 +1067,13 @@
      * @param needsReschedule Whether the implementing class should reschedule this job.
      */
     @Override
-    public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
+    public void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule) {
         if (DEBUG) {
             Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
         }
         // Do not write back immediately if this is a periodic job. The job may get lost if system
         // shuts down before it is added back.
-        if (!stopTrackingJob(jobStatus, null, !jobStatus.getJob().isPeriodic())) {
+        if (!stopTrackingJobLocked(jobStatus, null, !jobStatus.getJob().isPeriodic())) {
             if (DEBUG) {
                 Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
             }
@@ -1085,24 +1087,24 @@
         // the old job after scheduling the new one, but since we have no lock held here
         // that may cause ordering problems if the app removes jobStatus while in here.
         if (needsReschedule) {
-            JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
+            JobStatus rescheduled = getRescheduleJobForFailureLocked(jobStatus);
             try {
-                rescheduled.prepare(ActivityManager.getService());
+                rescheduled.prepareLocked(ActivityManager.getService());
             } catch (SecurityException e) {
                 Slog.w(TAG, "Unable to regrant job permissions for " + rescheduled);
             }
-            startTrackingJob(rescheduled, jobStatus);
+            startTrackingJobLocked(rescheduled, jobStatus);
         } else if (jobStatus.getJob().isPeriodic()) {
             JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
             try {
-                rescheduledPeriodic.prepare(ActivityManager.getService());
+                rescheduledPeriodic.prepareLocked(ActivityManager.getService());
             } catch (SecurityException e) {
                 Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledPeriodic);
             }
-            startTrackingJob(rescheduledPeriodic, jobStatus);
+            startTrackingJobLocked(rescheduledPeriodic, jobStatus);
         }
-        jobStatus.unprepare(ActivityManager.getService());
-        reportActive();
+        jobStatus.unprepareLocked(ActivityManager.getService());
+        reportActiveLocked();
         mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
     }
 
@@ -1410,7 +1412,7 @@
                     Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs.");
                 }
                 assignJobsToContextsLocked();
-                reportActive();
+                reportActiveLocked();
             }
         }
     }
@@ -1698,7 +1700,37 @@
 
             long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.schedule(job, uid);
+                return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, -1, null);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        // IJobScheduler implementation
+        @Override
+        public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException {
+            if (DEBUG) {
+                Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
+            }
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+
+            enforceValidJobRequest(uid, job);
+            if (job.isPersisted()) {
+                throw new IllegalArgumentException("Can't enqueue work for persisted jobs");
+            }
+            if (work == null) {
+                throw new NullPointerException("work is null");
+            }
+
+            if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
+            }
+
+            long ident = Binder.clearCallingIdentity();
+            try {
+                return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, -1, null);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -1731,7 +1763,7 @@
 
             long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.scheduleAsPackage(job, callerUid,
+                return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
                         packageName, userId, tag);
             } finally {
                 Binder.restoreCallingIdentity(ident);
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
index 728ed72d..c7ef0e2 100644
--- a/services/core/java/com/android/server/job/JobServiceContext.java
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -21,11 +21,11 @@
 import android.app.job.JobParameters;
 import android.app.job.IJobCallback;
 import android.app.job.IJobService;
+import android.app.job.JobWorkItem;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Handler;
@@ -195,7 +195,7 @@
             mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();
 
             mVerb = VERB_BINDING;
-            scheduleOpTimeOut();
+            scheduleOpTimeOutLocked();
             final Intent intent = new Intent().setComponent(job.getServiceComponent());
             boolean binding = mContext.bindServiceAsUser(intent, this,
                     Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND,
@@ -208,7 +208,7 @@
                 mParams = null;
                 mExecutionStartTimeElapsed = 0L;
                 mVerb = VERB_FINISHED;
-                removeOpTimeOut();
+                removeOpTimeOutLocked();
                 return false;
             }
             try {
@@ -297,6 +297,38 @@
         mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget();
     }
 
+    @Override
+    public JobWorkItem dequeueWork(int jobId) {
+        if (!verifyCallingUid()) {
+            throw new SecurityException("Bad calling uid: " + Binder.getCallingUid());
+        }
+        JobWorkItem work = null;
+        boolean stillWorking = false;
+        synchronized (mLock) {
+            if (mRunningJob != null) {
+                work = mRunningJob.dequeueWorkLocked();
+                stillWorking = mRunningJob.hasExecutingWorkLocked();
+            }
+        }
+        if (work == null && !stillWorking) {
+            jobFinished(jobId, false);
+        }
+        return work;
+    }
+
+    @Override
+    public boolean completeWork(int jobId, int workId) {
+        if (!verifyCallingUid()) {
+            throw new SecurityException("Bad calling uid: " + Binder.getCallingUid());
+        }
+        synchronized (mLock) {
+            if (mRunningJob != null) {
+                return mRunningJob.completeWorkLocked(workId);
+            }
+            return false;
+        }
+    }
+
     /**
      * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
      * we intend to send to the client - we stop sending work when the service is unbound so until
@@ -378,46 +410,18 @@
         public void handleMessage(Message message) {
             switch (message.what) {
                 case MSG_SERVICE_BOUND:
-                    removeOpTimeOut();
-                    handleServiceBoundH();
+                    doServiceBound();
                     break;
                 case MSG_CALLBACK:
-                    if (DEBUG) {
-                        Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob
-                                + " v:" + VERB_STRINGS[mVerb]);
-                    }
-                    removeOpTimeOut();
-
-                    if (mVerb == VERB_STARTING) {
-                        final boolean workOngoing = message.arg2 == 1;
-                        handleStartedH(workOngoing);
-                    } else if (mVerb == VERB_EXECUTING ||
-                            mVerb == VERB_STOPPING) {
-                        final boolean reschedule = message.arg2 == 1;
-                        handleFinishedH(reschedule);
-                    } else {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
-                        }
-                    }
+                    doCallback(message.arg2);
                     break;
                 case MSG_CANCEL:
-                    if (mVerb == VERB_FINISHED) {
-                        if (DEBUG) {
-                            Slog.d(TAG,
-                                   "Trying to process cancel for torn-down context, ignoring.");
-                        }
-                        return;
-                    }
-                    mParams.setStopReason(message.arg1);
-                    if (message.arg1 == JobParameters.REASON_PREEMPT) {
-                        mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
-                                NO_PREFERRED_UID;
-                    }
-                    handleCancelH();
+                    doCancel(message.arg1);
                     break;
                 case MSG_TIMEOUT:
-                    handleOpTimeoutH();
+                    synchronized (mLock) {
+                        handleOpTimeoutH();
+                    }
                     break;
                 case MSG_SHUTDOWN_EXECUTION:
                     closeAndCleanupJobH(true /* needsReschedule */);
@@ -427,6 +431,55 @@
             }
         }
 
+        void doServiceBound() {
+            synchronized (mLock) {
+                removeOpTimeOutLocked();
+                handleServiceBoundH();
+            }
+        }
+
+        void doCallback(int arg2) {
+            synchronized (mLock) {
+                if (DEBUG) {
+                    Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob
+                            + " v:" + VERB_STRINGS[mVerb]);
+                }
+                removeOpTimeOutLocked();
+
+                if (mVerb == VERB_STARTING) {
+                    final boolean workOngoing = arg2 == 1;
+                    handleStartedH(workOngoing);
+                } else if (mVerb == VERB_EXECUTING ||
+                        mVerb == VERB_STOPPING) {
+                    final boolean reschedule = arg2 == 1;
+                    handleFinishedH(reschedule);
+                } else {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
+                    }
+                }
+            }
+        }
+
+        void doCancel(int arg1) {
+            synchronized (mLock) {
+                if (mVerb == VERB_FINISHED) {
+                    if (DEBUG) {
+                        Slog.d(TAG,
+                                "Trying to process cancel for torn-down context, ignoring.");
+                    }
+                    return;
+                }
+                mParams.setStopReason(arg1);
+                if (arg1 == JobParameters.REASON_PREEMPT) {
+                    mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
+                            NO_PREFERRED_UID;
+                }
+                handleCancelH();
+            }
+
+        }
+
         /** Start the job on the service. */
         private void handleServiceBoundH() {
             if (DEBUG) {
@@ -448,7 +501,7 @@
             }
             try {
                 mVerb = VERB_STARTING;
-                scheduleOpTimeOut();
+                scheduleOpTimeOutLocked();
                 service.startJob(mParams);
             } catch (Exception e) {
                 // We catch 'Exception' because client-app malice or bugs might induce a wide
@@ -483,7 +536,7 @@
                         handleCancelH();
                         return;
                     }
-                    scheduleOpTimeOut();
+                    scheduleOpTimeOutLocked();
                     break;
                 default:
                     Slog.e(TAG, "Handling started job but job wasn't starting! Was "
@@ -587,7 +640,7 @@
          * VERB_STOPPING.
          */
         private void sendStopMessageH() {
-            removeOpTimeOut();
+            removeOpTimeOutLocked();
             if (mVerb != VERB_EXECUTING) {
                 Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
                 closeAndCleanupJobH(false /* reschedule */);
@@ -595,7 +648,7 @@
             }
             try {
                 mVerb = VERB_STOPPING;
-                scheduleOpTimeOut();
+                scheduleOpTimeOutLocked();
                 service.stopJob(mParams);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error sending onStopJob to client.", e);
@@ -635,13 +688,13 @@
                 mCancelled.set(false);
                 service = null;
                 mAvailable = true;
+                removeOpTimeOutLocked();
+                removeMessages(MSG_CALLBACK);
+                removeMessages(MSG_SERVICE_BOUND);
+                removeMessages(MSG_CANCEL);
+                removeMessages(MSG_SHUTDOWN_EXECUTION);
+                mCompletedListener.onJobCompletedLocked(completedJob, reschedule);
             }
-            removeOpTimeOut();
-            removeMessages(MSG_CALLBACK);
-            removeMessages(MSG_SERVICE_BOUND);
-            removeMessages(MSG_CANCEL);
-            removeMessages(MSG_SHUTDOWN_EXECUTION);
-            mCompletedListener.onJobCompleted(completedJob, reschedule);
         }
     }
 
@@ -650,8 +703,8 @@
      * we haven't received a response in a certain amount of time, we want to give up and carry
      * on with life.
      */
-    private void scheduleOpTimeOut() {
-        removeOpTimeOut();
+    private void scheduleOpTimeOutLocked() {
+        removeOpTimeOutLocked();
 
         final long timeoutMillis;
         switch (mVerb) {
@@ -678,7 +731,7 @@
     }
 
 
-    private void removeOpTimeOut() {
+    private void removeOpTimeOutLocked() {
         mCallbackHandler.removeMessages(MSG_TIMEOUT);
     }
 }
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index c0264df..fcc0827 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -454,7 +454,7 @@
                         IActivityManager am = ActivityManager.getService();
                         for (int i=0; i<jobs.size(); i++) {
                             JobStatus js = jobs.get(i);
-                            js.prepare(am);
+                            js.prepareLocked(am);
                             this.jobSet.add(js);
                         }
                     }
diff --git a/services/core/java/com/android/server/job/controllers/ContentObserverController.java b/services/core/java/com/android/server/job/controllers/ContentObserverController.java
index 5d209fc..29f0e2c 100644
--- a/services/core/java/com/android/server/job/controllers/ContentObserverController.java
+++ b/services/core/java/com/android/server/job/controllers/ContentObserverController.java
@@ -164,7 +164,7 @@
                             && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
                         // We are stopping this job, but it is going to be replaced by this given
                         // incoming job.  We want to propagate our state over to it, so we don't
-                        // lose any content changes that had happend since the last one started.
+                        // lose any content changes that had happened since the last one started.
                         // If there is a previous job associated with the new job, propagate over
                         // any pending content URI trigger reports.
                         if (incomingJob.contentObserverJobInstance == null) {
@@ -195,16 +195,14 @@
     }
 
     @Override
-    public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
+    public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) {
         if (failureToReschedule.hasContentTriggerConstraint()
                 && newJob.hasContentTriggerConstraint()) {
-            synchronized (mLock) {
-                // Our job has failed, and we are scheduling a new job for it.
-                // Copy the last reported content changes in to the new job, so when
-                // we schedule the new one we will pick them up and report them again.
-                newJob.changedAuthorities = failureToReschedule.changedAuthorities;
-                newJob.changedUris = failureToReschedule.changedUris;
-            }
+            // Our job has failed, and we are scheduling a new job for it.
+            // Copy the last reported content changes in to the new job, so when
+            // we schedule the new one we will pick them up and report them again.
+            newJob.changedAuthorities = failureToReschedule.changedAuthorities;
+            newJob.changedUris = failureToReschedule.changedUris;
         }
     }
 
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index d27d0e5..4cdce5f 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -19,6 +19,7 @@
 import android.app.AppGlobals;
 import android.app.IActivityManager;
 import android.app.job.JobInfo;
+import android.app.job.JobWorkItem;
 import android.content.ClipData;
 import android.content.ComponentName;
 import android.content.ContentProvider;
@@ -35,6 +36,7 @@
 import android.util.TimeUtils;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 
 /**
@@ -126,6 +128,14 @@
 
     public int lastEvaluatedPriority;
 
+    // If non-null, this is work that has been enqueued for the job.
+    public ArrayList<JobWorkItem> pendingWork;
+
+    // If non-null, this is work that is currently being executed.
+    public ArrayList<JobWorkItem> executingWork;
+
+    public int nextPendingWorkId = 1;
+
     // Used by shell commands
     public int overrideState = 0;
 
@@ -256,7 +266,59 @@
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis);
     }
 
-    public void prepare(IActivityManager am) {
+    public void enqueueWorkLocked(JobWorkItem work) {
+        if (pendingWork == null) {
+            pendingWork = new ArrayList<>();
+        }
+        work.setWorkId(nextPendingWorkId);
+        nextPendingWorkId++;
+        pendingWork.add(work);
+    }
+
+    public JobWorkItem dequeueWorkLocked() {
+        if (pendingWork != null && pendingWork.size() > 0) {
+            JobWorkItem work = pendingWork.remove(0);
+            if (work != null) {
+                if (executingWork == null) {
+                    executingWork = new ArrayList<>();
+                }
+                executingWork.add(work);
+            }
+            return work;
+        }
+        return null;
+    }
+
+    public boolean hasExecutingWorkLocked() {
+        return executingWork != null && executingWork.size() > 0;
+    }
+
+    public boolean completeWorkLocked(int workId) {
+        if (executingWork != null) {
+            final int N = executingWork.size();
+            for (int i = 0; i < N; i++) {
+                if (executingWork.get(i).getWorkId() == workId) {
+                    executingWork.remove(i);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public void stopTrackingJobLocked(JobStatus incomingJob) {
+        if (incomingJob != null) {
+            // We are replacing with a new job -- transfer the work!
+            incomingJob.pendingWork = pendingWork;
+            pendingWork = null;
+            incomingJob.nextPendingWorkId = nextPendingWorkId;
+        } else {
+            // We are completely stopping the job...  need to clean up work.
+            // XXX remove perms when that is impl.
+        }
+    }
+
+    public void prepareLocked(IActivityManager am) {
         if (prepared) {
             Slog.wtf(TAG, "Already prepared: " + this);
             return;
@@ -271,7 +333,7 @@
         }
     }
 
-    public void unprepare(IActivityManager am) {
+    public void unprepareLocked(IActivityManager am) {
         if (!prepared) {
             Slog.wtf(TAG, "Hasn't been prepared: " + this);
             return;
@@ -288,7 +350,7 @@
         }
     }
 
-    public boolean isPrepared() {
+    public boolean isPreparedLocked() {
         return prepared;
     }
 
@@ -854,6 +916,22 @@
                 }
             }
         }
+        if (pendingWork != null && pendingWork.size() > 0) {
+            pw.print(prefix); pw.println("Pending work:");
+            for (int i = 0; i < pendingWork.size(); i++) {
+                JobWorkItem work = pendingWork.get(i);
+                pw.print(prefix); pw.print("  #"); pw.print(i); pw.print(": #");
+                pw.print(work.getWorkId()); pw.print(" "); pw.println(work.getIntent());
+            }
+        }
+        if (executingWork != null && executingWork.size() > 0) {
+            pw.print(prefix); pw.println("Executing work:");
+            for (int i = 0; i < executingWork.size(); i++) {
+                JobWorkItem work = executingWork.get(i);
+                pw.print(prefix); pw.print("  #"); pw.print(i); pw.print(": #");
+                pw.print(work.getWorkId()); pw.print(" "); pw.println(work.getIntent());
+            }
+        }
         pw.print(prefix); pw.print("Earliest run time: ");
         pw.println(formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME));
         pw.print(prefix); pw.print("Latest run time: ");
diff --git a/services/core/java/com/android/server/job/controllers/StateController.java b/services/core/java/com/android/server/job/controllers/StateController.java
index 1721fb9..497faab 100644
--- a/services/core/java/com/android/server/job/controllers/StateController.java
+++ b/services/core/java/com/android/server/job/controllers/StateController.java
@@ -61,7 +61,7 @@
     /**
      * Called when a new job is being created to reschedule an old failed job.
      */
-    public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
+    public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) {
     }
 
     public abstract void dumpControllerStateLocked(PrintWriter pw, int filterUid);