Add new "work queue" feature to JobScheduler.
This gives semantics similar to the start command
queue of services.
The implementation is currently lacking in URI permission
grant handling of the work intents; that will be coming
in a follow-up change.
This includes a first step of adjusting/fixing locking
within JobSchedulerService. The JobServiceContext class
has a bunch of stuff it does that assumes it doesn't need
locking because it schedules the work on a handler. However,
to be able to correctly implement the work finish flow (that
takes care of stopping the job when there is no more work),
we can't dispatch these asynchronously so need to get rid of
that and just do explicit locking.
The switch to explicit locking is half-way there (again the
remaining part will be a follow-on CL). Right now we have
the locking, but still also the handler. But it turns out
there were a number of things we were doing without a lock
held where we actually should have been holding a lock, so
this is better anyway.
Test: new tests added
Change-Id: Iebd098046209b28e60fd2f4d855d7f91cd3a8b03
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},