summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt14
-rw-r--r--api/system-current.txt14
-rw-r--r--api/test-current.txt14
-rw-r--r--core/java/android/app/job/JobInfo.java120
-rw-r--r--core/java/android/app/job/JobParameters.java37
-rw-r--r--core/java/android/content/ContentResolver.java6
-rw-r--r--services/core/java/com/android/server/job/JobSchedulerService.java75
-rw-r--r--services/core/java/com/android/server/job/JobServiceContext.java14
-rw-r--r--services/core/java/com/android/server/job/JobStore.java15
-rw-r--r--services/core/java/com/android/server/job/controllers/AppIdleController.java4
-rw-r--r--services/core/java/com/android/server/job/controllers/BatteryController.java6
-rw-r--r--services/core/java/com/android/server/job/controllers/ConnectivityController.java4
-rw-r--r--services/core/java/com/android/server/job/controllers/ContentObserverController.java306
-rw-r--r--services/core/java/com/android/server/job/controllers/IdleController.java4
-rw-r--r--services/core/java/com/android/server/job/controllers/JobStatus.java151
-rw-r--r--services/core/java/com/android/server/job/controllers/StateController.java14
-rw-r--r--services/core/java/com/android/server/job/controllers/TimeController.java6
17 files changed, 754 insertions, 50 deletions
diff --git a/api/current.txt b/api/current.txt
index f71aa8473ed1..b895dfe9aa80 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6191,6 +6191,7 @@ package android.app.job {
method public long getMinLatencyMillis();
method public int getNetworkType();
method public android.content.ComponentName getService();
+ method public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
method public boolean isPeriodic();
method public boolean isPersisted();
method public boolean isRequireCharging();
@@ -6210,6 +6211,7 @@ package android.app.job {
public static final class JobInfo.Builder {
ctor public JobInfo.Builder(int, android.content.ComponentName);
+ method public android.app.job.JobInfo.Builder addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
@@ -6223,10 +6225,22 @@ package android.app.job {
method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
}
+ public static final class JobInfo.TriggerContentUri implements android.os.Parcelable {
+ ctor public JobInfo.TriggerContentUri(android.net.Uri, int);
+ method public int describeContents();
+ method public int getFlags();
+ method public android.net.Uri getUri();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.app.job.JobInfo.TriggerContentUri> CREATOR;
+ field public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1; // 0x1
+ }
+
public class JobParameters implements android.os.Parcelable {
method public int describeContents();
method public android.os.PersistableBundle getExtras();
method public int getJobId();
+ method public java.lang.String[] getTriggeredContentAuthorities();
+ method public android.net.Uri[] getTriggeredContentUris();
method public boolean isOverrideDeadlineExpired();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.app.job.JobParameters> CREATOR;
diff --git a/api/system-current.txt b/api/system-current.txt
index d9051c0aeee6..f9f13fcc39de 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -6446,6 +6446,7 @@ package android.app.job {
method public long getMinLatencyMillis();
method public int getNetworkType();
method public android.content.ComponentName getService();
+ method public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
method public boolean isPeriodic();
method public boolean isPersisted();
method public boolean isRequireCharging();
@@ -6465,6 +6466,7 @@ package android.app.job {
public static final class JobInfo.Builder {
ctor public JobInfo.Builder(int, android.content.ComponentName);
+ method public android.app.job.JobInfo.Builder addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
@@ -6478,10 +6480,22 @@ package android.app.job {
method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
}
+ public static final class JobInfo.TriggerContentUri implements android.os.Parcelable {
+ ctor public JobInfo.TriggerContentUri(android.net.Uri, int);
+ method public int describeContents();
+ method public int getFlags();
+ method public android.net.Uri getUri();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.app.job.JobInfo.TriggerContentUri> CREATOR;
+ field public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1; // 0x1
+ }
+
public class JobParameters implements android.os.Parcelable {
method public int describeContents();
method public android.os.PersistableBundle getExtras();
method public int getJobId();
+ method public java.lang.String[] getTriggeredContentAuthorities();
+ method public android.net.Uri[] getTriggeredContentUris();
method public boolean isOverrideDeadlineExpired();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.app.job.JobParameters> CREATOR;
diff --git a/api/test-current.txt b/api/test-current.txt
index 786a7c7c8d0d..18e6a1c41cca 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -6193,6 +6193,7 @@ package android.app.job {
method public long getMinLatencyMillis();
method public int getNetworkType();
method public android.content.ComponentName getService();
+ method public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
method public boolean isPeriodic();
method public boolean isPersisted();
method public boolean isRequireCharging();
@@ -6212,6 +6213,7 @@ package android.app.job {
public static final class JobInfo.Builder {
ctor public JobInfo.Builder(int, android.content.ComponentName);
+ method public android.app.job.JobInfo.Builder addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
@@ -6225,10 +6227,22 @@ package android.app.job {
method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
}
+ public static final class JobInfo.TriggerContentUri implements android.os.Parcelable {
+ ctor public JobInfo.TriggerContentUri(android.net.Uri, int);
+ method public int describeContents();
+ method public int getFlags();
+ method public android.net.Uri getUri();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.app.job.JobInfo.TriggerContentUri> CREATOR;
+ field public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1; // 0x1
+ }
+
public class JobParameters implements android.os.Parcelable {
method public int describeContents();
method public android.os.PersistableBundle getExtras();
method public int getJobId();
+ method public java.lang.String[] getTriggeredContentAuthorities();
+ method public android.net.Uri[] getTriggeredContentUris();
method public boolean isOverrideDeadlineExpired();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.app.job.JobParameters> CREATOR;
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 9ad35d4c1556..0143797dac29 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -16,11 +16,16 @@
package android.app.job;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.ComponentName;
+import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
+import java.util.ArrayList;
+
/**
* Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the
* parameters required to schedule work against the calling application. These are constructed
@@ -80,6 +85,7 @@ public class JobInfo implements Parcelable {
private final ComponentName service;
private final boolean requireCharging;
private final boolean requireDeviceIdle;
+ private final TriggerContentUri[] triggerContentUris;
private final boolean hasEarlyConstraint;
private final boolean hasLateConstraint;
private final int networkType;
@@ -134,6 +140,15 @@ public class JobInfo implements Parcelable {
}
/**
+ * Which content: URIs must change for the job to be scheduled. Returns null
+ * if there are none required.
+ */
+ @Nullable
+ public TriggerContentUri[] getTriggerContentUris() {
+ return triggerContentUris;
+ }
+
+ /**
* One of {@link android.app.job.JobInfo#NETWORK_TYPE_ANY},
* {@link android.app.job.JobInfo#NETWORK_TYPE_NONE}, or
* {@link android.app.job.JobInfo#NETWORK_TYPE_UNMETERED}.
@@ -232,6 +247,7 @@ public class JobInfo implements Parcelable {
service = in.readParcelable(null);
requireCharging = in.readInt() == 1;
requireDeviceIdle = in.readInt() == 1;
+ triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
networkType = in.readInt();
minLatencyMillis = in.readLong();
maxExecutionDelayMillis = in.readLong();
@@ -252,6 +268,9 @@ public class JobInfo implements Parcelable {
service = b.mJobService;
requireCharging = b.mRequiresCharging;
requireDeviceIdle = b.mRequiresDeviceIdle;
+ triggerContentUris = b.mTriggerContentUris != null
+ ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()])
+ : null;
networkType = b.mNetworkType;
minLatencyMillis = b.mMinLatencyMillis;
maxExecutionDelayMillis = b.mMaxExecutionDelayMillis;
@@ -278,6 +297,7 @@ public class JobInfo implements Parcelable {
out.writeParcelable(service, flags);
out.writeInt(requireCharging ? 1 : 0);
out.writeInt(requireDeviceIdle ? 1 : 0);
+ out.writeTypedArray(triggerContentUris, flags);
out.writeInt(networkType);
out.writeLong(minLatencyMillis);
out.writeLong(maxExecutionDelayMillis);
@@ -309,6 +329,75 @@ public class JobInfo implements Parcelable {
return "(job:" + jobId + "/" + service.flattenToShortString() + ")";
}
+ /**
+ * Information about a content URI modification that a job would like to
+ * trigger on.
+ */
+ public static final class TriggerContentUri implements Parcelable {
+ private final Uri mUri;
+ private final int mFlags;
+
+ /**
+ * Flag for trigger: also trigger if any descendants of the given URI change.
+ * Corresponds to the <var>notifyForDescendants</var> of
+ * {@link android.content.ContentResolver#registerContentObserver}.
+ */
+ public static final int FLAG_NOTIFY_FOR_DESCENDANTS = 1<<0;
+
+ /**
+ * Create a new trigger description.
+ * @param uri The URI to observe. Must be non-null.
+ * @param flags Optional flags for the observer, either 0 or
+ * {@link #FLAG_NOTIFY_FOR_DESCENDANTS}.
+ */
+ public TriggerContentUri(@NonNull Uri uri, int flags) {
+ mUri = uri;
+ mFlags = flags;
+ }
+
+ /**
+ * Return the Uri this trigger was created for.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Return the flags supplied for the trigger.
+ */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ private TriggerContentUri(Parcel in) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ mFlags = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ mUri.writeToParcel(out, flags);
+ out.writeInt(mFlags);
+ }
+
+ public static final Creator<TriggerContentUri> CREATOR = new Creator<TriggerContentUri>() {
+ @Override
+ public TriggerContentUri createFromParcel(Parcel in) {
+ return new TriggerContentUri(in);
+ }
+
+ @Override
+ public TriggerContentUri[] newArray(int size) {
+ return new TriggerContentUri[size];
+ }
+ };
+ }
+
/** Builder class for constructing {@link JobInfo} objects. */
public static final class Builder {
private int mJobId;
@@ -319,6 +408,7 @@ public class JobInfo implements Parcelable {
private boolean mRequiresCharging;
private boolean mRequiresDeviceIdle;
private int mNetworkType;
+ private ArrayList<TriggerContentUri> mTriggerContentUris;
private boolean mIsPersisted;
// One-off parameters.
private long mMinLatencyMillis;
@@ -403,6 +493,25 @@ public class JobInfo implements Parcelable {
}
/**
+ * Add a new content: URI that will be monitored with a
+ * {@link android.database.ContentObserver}, and will cause the job to execute if changed.
+ * If you have any trigger content URIs associated with a job, it will not execute until
+ * there has been a change report for one or more of them.
+ * <p>Note that trigger URIs can not be used in combination with
+ * {@link #setPeriodic(long)} or {@link #setPersisted(boolean)}. To continually monitor
+ * for content changes, you need to schedule a new JobInfo observing the same URIs
+ * before you finish execution of the JobService handling the most recent changes.</p>
+ * @param uri The content: URI to monitor.
+ */
+ public Builder addTriggerContentUri(@NonNull TriggerContentUri uri) {
+ if (mTriggerContentUris == null) {
+ mTriggerContentUris = new ArrayList<>();
+ }
+ mTriggerContentUris.add(uri);
+ return this;
+ }
+
+ /**
* Specify that this job should recur with the provided interval, not more than once per
* period. You have no control over when within this interval this job will be executed,
* only the guarantee that it will be executed at most once within this interval.
@@ -498,7 +607,8 @@ public class JobInfo implements Parcelable {
public JobInfo build() {
// Allow jobs with no constraints - What am I, a database?
if (!mHasEarlyConstraint && !mHasLateConstraint && !mRequiresCharging &&
- !mRequiresDeviceIdle && mNetworkType == NETWORK_TYPE_NONE) {
+ !mRequiresDeviceIdle && mNetworkType == NETWORK_TYPE_NONE &&
+ mTriggerContentUris == null) {
throw new IllegalArgumentException("You're trying to build a job with no " +
"constraints, this is not allowed.");
}
@@ -512,6 +622,14 @@ public class JobInfo implements Parcelable {
throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
"periodic job");
}
+ if (mIsPeriodic && (mTriggerContentUris != null)) {
+ throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+ "periodic job");
+ }
+ if (mIsPersisted && (mTriggerContentUris != null)) {
+ throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+ "persisted job");
+ }
if (mBackoffPolicySet && mRequiresDeviceIdle) {
throw new IllegalArgumentException("An idle mode job will not respect any" +
" back-off policy, so calling setBackoffCriteria with" +
diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java
index a0a60e8c53a0..8b309e1118e1 100644
--- a/core/java/android/app/job/JobParameters.java
+++ b/core/java/android/app/job/JobParameters.java
@@ -17,6 +17,7 @@
package android.app.job;
import android.app.job.IJobCallback;
+import android.net.Uri;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
@@ -43,15 +44,21 @@ public class JobParameters implements Parcelable {
private final PersistableBundle extras;
private final IBinder callback;
private final boolean overrideDeadlineExpired;
+ private final Uri[] mTriggeredContentUris;
+ private final String[] mTriggeredContentAuthorities;
+
private int stopReason; // Default value of stopReason is REASON_CANCELED
/** @hide */
public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
- boolean overrideDeadlineExpired) {
+ boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
+ String[] triggeredContentAuthorities) {
this.jobId = jobId;
this.extras = extras;
this.callback = callback;
this.overrideDeadlineExpired = overrideDeadlineExpired;
+ this.mTriggeredContentUris = triggeredContentUris;
+ this.mTriggeredContentAuthorities = triggeredContentAuthorities;
}
/**
@@ -88,6 +95,30 @@ public class JobParameters implements Parcelable {
return overrideDeadlineExpired;
}
+ /**
+ * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this
+ * reports which URIs have triggered the job. This will be null if either no URIs have
+ * triggered it (it went off due to a deadline or other reason), or the number of changed
+ * URIs is too large to report. Whether or not the number of URIs is too large, you can
+ * always use {@link #getTriggeredContentAuthorities()} to determine whether the job was
+ * triggered due to any content changes and the authorities they are associated with.
+ */
+ public Uri[] getTriggeredContentUris() {
+ return mTriggeredContentUris;
+ }
+
+ /**
+ * For jobs with {@link android.app.job.JobInfo.Builder#addTriggerContentUri} set, this
+ * reports which content authorities have triggered the job. It will only be null if no
+ * authorities have triggered it -- that is, the job executed for some other reason, such
+ * as a deadline expiring. If this is non-null, you can use {@link #getTriggeredContentUris()}
+ * to retrieve the details of which URIs changed (as long as that has not exceeded the maximum
+ * number it can reported).
+ */
+ public String[] getTriggeredContentAuthorities() {
+ return mTriggeredContentAuthorities;
+ }
+
/** @hide */
public IJobCallback getCallback() {
return IJobCallback.Stub.asInterface(callback);
@@ -98,6 +129,8 @@ public class JobParameters implements Parcelable {
extras = in.readPersistableBundle();
callback = in.readStrongBinder();
overrideDeadlineExpired = in.readInt() == 1;
+ mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
+ mTriggeredContentAuthorities = in.createStringArray();
stopReason = in.readInt();
}
@@ -117,6 +150,8 @@ public class JobParameters implements Parcelable {
dest.writePersistableBundle(extras);
dest.writeStrongBinder(callback);
dest.writeInt(overrideDeadlineExpired ? 1 : 0);
+ dest.writeTypedArray(mTriggeredContentUris, flags);
+ dest.writeStringArray(mTriggeredContentAuthorities);
dest.writeInt(stopReason);
}
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 684a85e52674..3461c11c4e9b 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -1592,20 +1592,20 @@ public abstract class ContentResolver {
*
* @param uri The URI to watch for changes. This can be a specific row URI, or a base URI
* for a whole class of content.
- * @param notifyForDescendents When false, the observer will be notified whenever a
+ * @param notifyForDescendants When false, the observer will be notified whenever a
* change occurs to the exact URI specified by <code>uri</code> or to one of the
* URI's ancestors in the path hierarchy. When true, the observer will also be notified
* whenever a change occurs to the URI's descendants in the path hierarchy.
* @param observer The object that receives callbacks when changes occur.
* @see #unregisterContentObserver
*/
- public final void registerContentObserver(@NonNull Uri uri, boolean notifyForDescendents,
+ public final void registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,
@NonNull ContentObserver observer) {
Preconditions.checkNotNull(uri, "uri");
Preconditions.checkNotNull(observer, "observer");
registerContentObserver(
ContentProvider.getUriWithoutUserId(uri),
- notifyForDescendents,
+ notifyForDescendants,
observer,
ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()));
}
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index c63ce7334918..43cd44fb7291 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -60,6 +60,7 @@ import com.android.server.LocalServices;
import com.android.server.job.controllers.AppIdleController;
import com.android.server.job.controllers.BatteryController;
import com.android.server.job.controllers.ConnectivityController;
+import com.android.server.job.controllers.ContentObserverController;
import com.android.server.job.controllers.IdleController;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.StateController;
@@ -109,6 +110,11 @@ public class JobSchedulerService extends com.android.server.SystemService
*/
static final int MIN_CONNECTIVITY_COUNT = 1; // Run connectivity jobs as soon as ready.
/**
+ * Minimum # of content trigger jobs that must be ready in order to force the JMS to schedule
+ * things early.
+ */
+ static final int MIN_CONTENT_COUNT = 1;
+ /**
* Minimum # of jobs (with no particular constraints) for which the JMS will be happy running
* some work early.
* This is correlated with the amount of batching we'll be able to do.
@@ -229,7 +235,6 @@ public class JobSchedulerService extends com.android.server.SystemService
if (packageName != null) {
jobStatus.setSource(packageName, userId);
}
- cancelJob(uId, job.getId());
try {
if (ActivityManagerNative.getDefault().getAppStartMode(uId,
job.getService().getPackageName()) == ActivityManager.APP_START_MODE_DISABLED) {
@@ -239,7 +244,15 @@ public class JobSchedulerService extends com.android.server.SystemService
}
} catch (RemoteException e) {
}
- startTrackingJob(jobStatus);
+ if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString());
+ JobStatus toCancel;
+ synchronized (mJobs) {
+ toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
+ }
+ startTrackingJob(jobStatus, toCancel);
+ if (toCancel != null) {
+ cancelJobImpl(toCancel);
+ }
mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
return JobScheduler.RESULT_SUCCESS;
}
@@ -316,9 +329,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
private void cancelJobImpl(JobStatus cancelled) {
- if (DEBUG) {
- Slog.d(TAG, "Cancelling: " + cancelled);
- }
+ if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
stopTrackingJob(cancelled);
synchronized (mJobs) {
// Remove from pending queue.
@@ -410,6 +421,7 @@ public class JobSchedulerService extends com.android.server.SystemService
mControllers.add(IdleController.get(this));
mControllers.add(BatteryController.get(this));
mControllers.add(AppIdleController.get(this));
+ mControllers.add(ContentObserverController.get(this));
mHandler = new JobHandler(context.getMainLooper());
mJobSchedulerStub = new JobSchedulerStub();
@@ -461,7 +473,7 @@ public class JobSchedulerService extends com.android.server.SystemService
JobStatus job = jobs.valueAt(i);
for (int controller=0; controller<mControllers.size(); controller++) {
mControllers.get(controller).deviceIdleModeChanged(mDeviceIdleMode);
- mControllers.get(controller).maybeStartTrackingJob(job);
+ mControllers.get(controller).maybeStartTrackingJob(job, null);
}
}
// GO GO GO!
@@ -475,7 +487,7 @@ public class JobSchedulerService extends com.android.server.SystemService
* {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
* about.
*/
- private void startTrackingJob(JobStatus jobStatus) {
+ private void startTrackingJob(JobStatus jobStatus, JobStatus lastJob) {
boolean update;
boolean rocking;
synchronized (mJobs) {
@@ -486,9 +498,9 @@ public class JobSchedulerService extends com.android.server.SystemService
for (int i=0; i<mControllers.size(); i++) {
StateController controller = mControllers.get(i);
if (update) {
- controller.maybeStopTrackingJob(jobStatus);
+ controller.maybeStopTrackingJob(jobStatus, true);
}
- controller.maybeStartTrackingJob(jobStatus);
+ controller.maybeStartTrackingJob(jobStatus, lastJob);
}
}
}
@@ -508,7 +520,7 @@ public class JobSchedulerService extends com.android.server.SystemService
if (removed && rocking) {
for (int i=0; i<mControllers.size(); i++) {
StateController controller = mControllers.get(i);
- controller.maybeStopTrackingJob(jobStatus);
+ controller.maybeStopTrackingJob(jobStatus, false);
}
}
return removed;
@@ -577,8 +589,13 @@ public class JobSchedulerService extends com.android.server.SystemService
}
delayMillis =
Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
- return new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis,
+ JobStatus newJob = new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis,
JobStatus.NO_LATEST_RUNTIME, backoffAttempts);
+ for (int ic=0; ic<mControllers.size(); ic++) {
+ StateController controller = mControllers.get(ic);
+ controller.rescheduleForFailure(newJob, failureToReschedule);
+ }
+ return newJob;
}
/**
@@ -632,14 +649,21 @@ public class JobSchedulerService extends com.android.server.SystemService
if (DEBUG) {
Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
}
+ // We still want to check for jobs to execute, because this job may have
+ // scheduled a new job under the same job id, and now we can run it.
+ mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
return;
}
+ // Note: there is a small window of time in here where, when rescheduling a job,
+ // we will stop monitoring its content providers. This should be fixed by stopping
+ // 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);
- startTrackingJob(rescheduled);
+ startTrackingJob(rescheduled, jobStatus);
} else if (jobStatus.getJob().isPeriodic()) {
JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
- startTrackingJob(rescheduledPeriodic);
+ startTrackingJob(rescheduledPeriodic, jobStatus);
}
reportActive();
mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
@@ -760,8 +784,10 @@ public class JobSchedulerService extends com.android.server.SystemService
int idleCount = 0;
int backoffCount = 0;
int connectivityCount = 0;
+ int contentCount = 0;
List<JobStatus> runnableJobs = null;
ArraySet<JobStatus> jobs = mJobs.getJobs();
+ if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs...");
for (int i=0; i<jobs.size(); i++) {
JobStatus job = jobs.valueAt(i);
if (isReadyToBeExecutedLocked(job)) {
@@ -788,6 +814,9 @@ public class JobSchedulerService extends com.android.server.SystemService
if (job.hasChargingConstraint()) {
chargingCount++;
}
+ if (job.hasContentTriggerConstraint()) {
+ contentCount++;
+ }
if (runnableJobs == null) {
runnableJobs = new ArrayList<>();
}
@@ -801,6 +830,7 @@ public class JobSchedulerService extends com.android.server.SystemService
idleCount >= MIN_IDLE_COUNT ||
connectivityCount >= MIN_CONNECTIVITY_COUNT ||
chargingCount >= MIN_CHARGING_COUNT ||
+ contentCount >= MIN_CONTENT_COUNT ||
(runnableJobs != null && runnableJobs.size() >= MIN_READY_JOBS_COUNT)) {
if (DEBUG) {
Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs.");
@@ -953,6 +983,10 @@ public class JobSchedulerService extends com.android.server.SystemService
Slog.d(TAG, "About to run job on context "
+ String.valueOf(i) + ", job: " + contextIdToJobMap[i]);
}
+ for (int ic=0; ic<mControllers.size(); ic++) {
+ StateController controller = mControllers.get(ic);
+ controller.prepareForExecution(contextIdToJobMap[i]);
+ }
if (!mActiveServices.get(i).executeRunnableJob(contextIdToJobMap[i])) {
Slog.d(TAG, "Error executing " + contextIdToJobMap[i]);
}
@@ -1162,7 +1196,20 @@ public class JobSchedulerService extends com.android.server.SystemService
ArraySet<JobStatus> jobs = mJobs.getJobs();
for (int i=0; i<jobs.size(); i++) {
JobStatus job = jobs.valueAt(i);
- job.dump(pw, " ");
+ pw.print(" Job #"); pw.print(i); pw.print(": ");
+ pw.println(job.toShortString());
+ job.dump(pw, " ");
+ pw.print(" Ready: ");
+ pw.print(mHandler.isReadyToBeExecutedLocked(job));
+ pw.print(" (job=");
+ pw.print(job.isReady());
+ pw.print(" pending=");
+ pw.print(mPendingJobs.contains(job));
+ pw.print(" active=");
+ pw.print(isCurrentlyActiveLocked(job));
+ pw.print(" user=");
+ pw.print(mStartedUsers.contains(job.getUserId()));
+ pw.println(")");
}
} else {
pw.println(" None.");
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
index dd634fa270c8..b249739811a6 100644
--- a/services/core/java/com/android/server/job/JobServiceContext.java
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -25,6 +25,7 @@ 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;
import android.os.IBinder;
@@ -170,7 +171,18 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
final boolean isDeadlineExpired =
job.hasDeadlineConstraint() &&
(job.getLatestRunTimeElapsed() < SystemClock.elapsedRealtime());
- mParams = new JobParameters(this, job.getJobId(), job.getExtras(), isDeadlineExpired);
+ Uri[] triggeredUris = null;
+ if (job.changedUris != null) {
+ triggeredUris = new Uri[job.changedUris.size()];
+ job.changedUris.toArray(triggeredUris);
+ }
+ String[] triggeredAuthorities = null;
+ if (job.changedAuthorities != null) {
+ triggeredAuthorities = new String[job.changedAuthorities.size()];
+ job.changedAuthorities.toArray(triggeredAuthorities);
+ }
+ mParams = new JobParameters(this, job.getJobId(), job.getExtras(), isDeadlineExpired,
+ triggeredUris, triggeredAuthorities);
mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();
mVerb = VERB_BINDING;
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index c88f5d7a82d8..f796164ef8ce 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -187,9 +187,8 @@ public class JobStore {
*/
public List<JobStatus> getJobsByUser(int userHandle) {
List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
- Iterator<JobStatus> it = mJobSet.iterator();
- while (it.hasNext()) {
- JobStatus ts = it.next();
+ for (int i=mJobSet.size()-1; i>=0; i--) {
+ JobStatus ts = mJobSet.valueAt(i);
if (UserHandle.getUserId(ts.getUid()) == userHandle) {
matchingJobs.add(ts);
}
@@ -203,9 +202,8 @@ public class JobStore {
*/
public List<JobStatus> getJobsByUid(int uid) {
List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
- Iterator<JobStatus> it = mJobSet.iterator();
- while (it.hasNext()) {
- JobStatus ts = it.next();
+ for (int i=mJobSet.size()-1; i>=0; i--) {
+ JobStatus ts = mJobSet.valueAt(i);
if (ts.getUid() == uid) {
matchingJobs.add(ts);
}
@@ -219,9 +217,8 @@ public class JobStore {
* @return the JobStatus that matches the provided uId and jobId, or null if none found.
*/
public JobStatus getJobByUidAndJobId(int uid, int jobId) {
- Iterator<JobStatus> it = mJobSet.iterator();
- while (it.hasNext()) {
- JobStatus ts = it.next();
+ for (int i=mJobSet.size()-1; i>=0; i--) {
+ JobStatus ts = mJobSet.valueAt(i);
if (ts.getUid() == uid && ts.getJobId() == jobId) {
return ts;
}
diff --git a/services/core/java/com/android/server/job/controllers/AppIdleController.java b/services/core/java/com/android/server/job/controllers/AppIdleController.java
index c09e06cc7311..5f3da7577a5d 100644
--- a/services/core/java/com/android/server/job/controllers/AppIdleController.java
+++ b/services/core/java/com/android/server/job/controllers/AppIdleController.java
@@ -62,7 +62,7 @@ public class AppIdleController extends StateController {
}
@Override
- public void maybeStartTrackingJob(JobStatus jobStatus) {
+ public void maybeStartTrackingJob(JobStatus jobStatus, JobStatus lastJob) {
synchronized (mTrackedTasks) {
mTrackedTasks.add(jobStatus);
String packageName = jobStatus.getSourcePackageName();
@@ -77,7 +77,7 @@ public class AppIdleController extends StateController {
}
@Override
- public void maybeStopTrackingJob(JobStatus jobStatus) {
+ public void maybeStopTrackingJob(JobStatus jobStatus, boolean forUpdate) {
synchronized (mTrackedTasks) {
mTrackedTasks.remove(jobStatus);
}
diff --git a/services/core/java/com/android/server/job/controllers/BatteryController.java b/services/core/java/com/android/server/job/controllers/BatteryController.java
index 7c2aead5cb07..b322a3e96c14 100644
--- a/services/core/java/com/android/server/job/controllers/BatteryController.java
+++ b/services/core/java/com/android/server/job/controllers/BatteryController.java
@@ -16,8 +16,6 @@
package com.android.server.job.controllers;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -79,7 +77,7 @@ public class BatteryController extends StateController {
}
@Override
- public void maybeStartTrackingJob(JobStatus taskStatus) {
+ public void maybeStartTrackingJob(JobStatus taskStatus, JobStatus lastJob) {
final boolean isOnStablePower = mChargeTracker.isOnStablePower();
if (taskStatus.hasChargingConstraint()) {
synchronized (mTrackedTasks) {
@@ -90,7 +88,7 @@ public class BatteryController extends StateController {
}
@Override
- public void maybeStopTrackingJob(JobStatus taskStatus) {
+ public void maybeStopTrackingJob(JobStatus taskStatus, boolean forUpdate) {
if (taskStatus.hasChargingConstraint()) {
synchronized (mTrackedTasks) {
mTrackedTasks.remove(taskStatus);
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
index daba0d9c36d3..b84658a7db96 100644
--- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -82,7 +82,7 @@ public class ConnectivityController extends StateController implements
}
@Override
- public void maybeStartTrackingJob(JobStatus jobStatus) {
+ public void maybeStartTrackingJob(JobStatus jobStatus, JobStatus lastJob) {
if (jobStatus.hasConnectivityConstraint() || jobStatus.hasUnmeteredConstraint()) {
synchronized (mTrackedJobs) {
jobStatus.connectivityConstraintSatisfied.set(mNetworkConnected);
@@ -93,7 +93,7 @@ public class ConnectivityController extends StateController implements
}
@Override
- public void maybeStopTrackingJob(JobStatus jobStatus) {
+ public void maybeStopTrackingJob(JobStatus jobStatus, boolean forUpdate) {
if (jobStatus.hasConnectivityConstraint() || jobStatus.hasUnmeteredConstraint()) {
synchronized (mTrackedJobs) {
mTrackedJobs.remove(jobStatus);
diff --git a/services/core/java/com/android/server/job/controllers/ContentObserverController.java b/services/core/java/com/android/server/job/controllers/ContentObserverController.java
new file mode 100644
index 000000000000..212cc949364d
--- /dev/null
+++ b/services/core/java/com/android/server/job/controllers/ContentObserverController.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2016 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.job.controllers;
+
+import android.app.job.JobInfo;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateChangedListener;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Controller for monitoring changes to content URIs through a ContentObserver.
+ */
+public class ContentObserverController extends StateController {
+ private static final String TAG = "JobScheduler.Content";
+
+ /**
+ * Maximum number of changing URIs we will batch together to report.
+ * XXX Should be smarter about this, restricting it by the maximum number
+ * of characters we will retain.
+ */
+ private static final int MAX_URIS_REPORTED = 50;
+
+ private static final Object sCreationLock = new Object();
+ private static volatile ContentObserverController sController;
+
+ final private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
+ ArrayMap<Uri, ObserverInstance> mObservers = new ArrayMap<>();
+ final Handler mHandler = new Handler();
+
+ public static ContentObserverController get(JobSchedulerService taskManagerService) {
+ synchronized (sCreationLock) {
+ if (sController == null) {
+ sController = new ContentObserverController(taskManagerService,
+ taskManagerService.getContext());
+ }
+ }
+ return sController;
+ }
+
+ @VisibleForTesting
+ public static ContentObserverController getForTesting(StateChangedListener stateChangedListener,
+ Context context) {
+ return new ContentObserverController(stateChangedListener, context);
+ }
+
+ private ContentObserverController(StateChangedListener stateChangedListener, Context context) {
+ super(stateChangedListener, context);
+ }
+
+ @Override
+ public void maybeStartTrackingJob(JobStatus taskStatus, JobStatus lastJob) {
+ if (taskStatus.hasContentTriggerConstraint()) {
+ synchronized (mTrackedTasks) {
+ if (taskStatus.contentObserverJobInstance == null) {
+ taskStatus.contentObserverJobInstance = new JobInstance(taskStatus);
+ }
+ mTrackedTasks.add(taskStatus);
+ boolean havePendingUris = false;
+ // If there is a previous job associated with the new job, propagate over
+ // any pending content URI trigger reports.
+ if (lastJob != null && lastJob.contentObserverJobInstance != null
+ && lastJob.contentObserverJobInstance
+ != taskStatus.contentObserverJobInstance
+ && lastJob.contentObserverJobInstance.mChangedAuthorities != null) {
+ havePendingUris = true;
+ taskStatus.contentObserverJobInstance.mChangedAuthorities
+ = lastJob.contentObserverJobInstance.mChangedAuthorities;
+ taskStatus.contentObserverJobInstance.mChangedUris
+ = lastJob.contentObserverJobInstance.mChangedUris;
+ lastJob.contentObserverJobInstance.mChangedAuthorities = null;
+ lastJob.contentObserverJobInstance.mChangedUris = null;
+ }
+ // If we have previously reported changed authorities/uris, then we failed
+ // to complete the job with them so will re-record them to report again.
+ if (taskStatus.changedAuthorities != null) {
+ havePendingUris = true;
+ if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) {
+ taskStatus.contentObserverJobInstance.mChangedAuthorities
+ = new ArraySet<>();
+ }
+ for (String auth : taskStatus.changedAuthorities) {
+ taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth);
+ }
+ if (taskStatus.changedUris != null) {
+ if (taskStatus.contentObserverJobInstance.mChangedUris == null) {
+ taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>();
+ }
+ for (Uri uri : taskStatus.changedUris) {
+ taskStatus.contentObserverJobInstance.mChangedUris.add(uri);
+ }
+ }
+ taskStatus.changedAuthorities = null;
+ taskStatus.changedUris = null;
+ }
+ taskStatus.changedAuthorities = null;
+ taskStatus.changedUris = null;
+ taskStatus.contentTriggerConstraintSatisfied.set(havePendingUris);
+ }
+ }
+ }
+
+ @Override
+ public void prepareForExecution(JobStatus taskStatus) {
+ if (taskStatus.hasContentTriggerConstraint()) {
+ synchronized (mTrackedTasks) {
+ if (taskStatus.contentObserverJobInstance != null) {
+ taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris;
+ taskStatus.changedAuthorities
+ = taskStatus.contentObserverJobInstance.mChangedAuthorities;
+ taskStatus.contentObserverJobInstance.mChangedUris = null;
+ taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJob(JobStatus taskStatus, boolean forUpdate) {
+ if (taskStatus.hasContentTriggerConstraint()) {
+ synchronized (mTrackedTasks) {
+ if (!forUpdate) {
+ // We won't do this reset if being called for an update, because
+ // we know it will be immediately followed by maybeStartTrackingJob...
+ // and we don't want to lose any content changes in-between.
+ if (taskStatus.contentObserverJobInstance != null) {
+ taskStatus.contentObserverJobInstance.detach();
+ taskStatus.contentObserverJobInstance = null;
+ }
+ }
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+ }
+
+ @Override
+ public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
+ if (failureToReschedule.hasContentTriggerConstraint()
+ && newJob.hasContentTriggerConstraint()) {
+ synchronized (mTrackedTasks) {
+ // 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;
+ }
+ }
+ }
+
+ class ObserverInstance extends ContentObserver {
+ final Uri mUri;
+ final ArrayList<JobInstance> mJobs = new ArrayList<>();
+
+ public ObserverInstance(Handler handler, Uri uri) {
+ super(handler);
+ mUri = uri;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ boolean reportChange = false;
+ synchronized (mTrackedTasks) {
+ final int N = mJobs.size();
+ for (int i=0; i<N; i++) {
+ JobInstance inst = mJobs.get(i);
+ if (inst.mChangedUris == null) {
+ inst.mChangedUris = new ArraySet<>();
+ }
+ if (inst.mChangedUris.size() < MAX_URIS_REPORTED) {
+ inst.mChangedUris.add(uri);
+ }
+ if (inst.mChangedAuthorities == null) {
+ inst.mChangedAuthorities = new ArraySet<>();
+ }
+ inst.mChangedAuthorities.add(uri.getAuthority());
+ boolean previous
+ = inst.mJobStatus.contentTriggerConstraintSatisfied.getAndSet(true);
+ if (!previous) {
+ reportChange = true;
+ }
+ }
+ }
+ // Let the scheduler know that state has changed. This may or may not result in an
+ // execution.
+ if (reportChange) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ }
+
+ class JobInstance extends ArrayList<ObserverInstance> {
+ private final JobStatus mJobStatus;
+ private ArraySet<Uri> mChangedUris;
+ private ArraySet<String> mChangedAuthorities;
+
+ JobInstance(JobStatus jobStatus) {
+ mJobStatus = jobStatus;
+ final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris();
+ if (uris != null) {
+ for (JobInfo.TriggerContentUri uri : uris) {
+ ObserverInstance obs = mObservers.get(uri.getUri());
+ if (obs == null) {
+ obs = new ObserverInstance(mHandler, uri.getUri());
+ mObservers.put(uri.getUri(), obs);
+ mContext.getContentResolver().registerContentObserver(
+ uri.getUri(),
+ (uri.getFlags() &
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)
+ != 0,
+ obs);
+ }
+ obs.mJobs.add(this);
+ add(obs);
+ }
+ }
+ }
+
+ void detach() {
+ final int N = size();
+ for (int i=0; i<N; i++) {
+ final ObserverInstance obs = get(i);
+ obs.mJobs.remove(this);
+ if (obs.mJobs.size() == 0) {
+ mContext.getContentResolver().unregisterContentObserver(obs);
+ mObservers.remove(obs.mUri);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dumpControllerState(PrintWriter pw) {
+ pw.println("Content.");
+ synchronized (mTrackedTasks) {
+ Iterator<JobStatus> it = mTrackedTasks.iterator();
+ if (it.hasNext()) {
+ pw.print(String.valueOf(it.next().hashCode()));
+ }
+ while (it.hasNext()) {
+ pw.print("," + String.valueOf(it.next().hashCode()));
+ }
+ pw.println();
+ int N = mObservers.size();
+ if (N > 0) {
+ pw.println("URIs:");
+ for (int i = 0; i < N; i++) {
+ ObserverInstance obs = mObservers.valueAt(i);
+ pw.print(" ");
+ pw.print(mObservers.keyAt(i));
+ pw.println(":");
+ pw.print(" ");
+ pw.println(obs);
+ pw.println(" Jobs:");
+ int M = obs.mJobs.size();
+ for (int j=0; j<M; j++) {
+ JobInstance inst = obs.mJobs.get(j);
+ pw.print(" ");
+ pw.print(inst.hashCode());
+ if (inst.mChangedAuthorities != null) {
+ pw.println(":");
+ pw.println(" Changed Authorities:");
+ for (int k=0; k<inst.mChangedAuthorities.size(); k++) {
+ pw.print(" ");
+ pw.println(inst.mChangedAuthorities.valueAt(k));
+ }
+ if (inst.mChangedUris != null) {
+ pw.println(" Changed URIs:");
+ for (int k = 0; k<inst.mChangedUris.size(); k++) {
+ pw.print(" ");
+ pw.println(inst.mChangedUris.valueAt(k));
+ }
+ }
+ } else {
+ pw.println();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/job/controllers/IdleController.java b/services/core/java/com/android/server/job/controllers/IdleController.java
index fe5e8c94bc3e..9f4cdef0e5f7 100644
--- a/services/core/java/com/android/server/job/controllers/IdleController.java
+++ b/services/core/java/com/android/server/job/controllers/IdleController.java
@@ -66,7 +66,7 @@ public class IdleController extends StateController {
* StateController interface
*/
@Override
- public void maybeStartTrackingJob(JobStatus taskStatus) {
+ public void maybeStartTrackingJob(JobStatus taskStatus, JobStatus lastJob) {
if (taskStatus.hasIdleConstraint()) {
synchronized (mTrackedTasks) {
mTrackedTasks.add(taskStatus);
@@ -76,7 +76,7 @@ public class IdleController extends StateController {
}
@Override
- public void maybeStopTrackingJob(JobStatus taskStatus) {
+ public void maybeStopTrackingJob(JobStatus taskStatus, boolean forUpdate) {
synchronized (mTrackedTasks) {
mTrackedTasks.remove(taskStatus);
}
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 e6e9e49030e9..41131800cd2f 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -19,11 +19,14 @@ package com.android.server.job.controllers;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.content.ComponentName;
+import android.net.Uri;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.TimeUtils;
import java.io.PrintWriter;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -61,6 +64,17 @@ public class JobStatus {
final AtomicBoolean unmeteredConstraintSatisfied = new AtomicBoolean();
final AtomicBoolean connectivityConstraintSatisfied = new AtomicBoolean();
final AtomicBoolean appNotIdleConstraintSatisfied = new AtomicBoolean();
+ final AtomicBoolean contentTriggerConstraintSatisfied = new AtomicBoolean();
+
+ // These are filled in by controllers when preparing for execution.
+ public ArraySet<Uri> changedUris;
+ public ArraySet<String> changedAuthorities;
+
+ /**
+ * For use only by ContentObserverController: state it is maintaining about content URIs
+ * being observed.
+ */
+ ContentObserverController.JobInstance contentObserverJobInstance;
/**
* Earliest point in the future at which this job will be eligible to run. A value of 0
@@ -220,6 +234,10 @@ public class JobStatus {
return job.isRequireDeviceIdle();
}
+ public boolean hasContentTriggerConstraint() {
+ return job.getTriggerContentUris() != null;
+ }
+
public boolean isPersisted() {
return job.isPersisted();
}
@@ -252,7 +270,8 @@ public class JobStatus {
&& (!hasTimingDelayConstraint() || timeDelayConstraintSatisfied.get())
&& (!hasConnectivityConstraint() || connectivityConstraintSatisfied.get())
&& (!hasUnmeteredConstraint() || unmeteredConstraintSatisfied.get())
- && (!hasIdleConstraint() || idleConstraintSatisfied.get());
+ && (!hasIdleConstraint() || idleConstraintSatisfied.get())
+ && (!hasContentTriggerConstraint() || contentTriggerConstraintSatisfied.get());
}
public boolean matches(int uid, int jobId) {
@@ -268,8 +287,9 @@ public class JobStatus {
+ ",R=(" + formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME)
+ "," + formatRunTime(latestRunTimeElapsedMillis, NO_LATEST_RUNTIME) + ")"
+ ",N=" + job.getNetworkType() + ",C=" + job.isRequireCharging()
- + ",I=" + job.isRequireDeviceIdle() + ",F=" + numFailures
- + ",P=" + job.isPersisted()
+ + ",I=" + job.isRequireDeviceIdle()
+ + ",U=" + (job.getTriggerContentUris() != null)
+ + ",F=" + numFailures + ",P=" + job.isPersisted()
+ ",ANI=" + appNotIdleConstraintSatisfied.get()
+ (isReady() ? "(READY)" : "")
+ "]";
@@ -310,13 +330,132 @@ public class JobStatus {
* {@link #toString()} returns.
*/
public String toShortString() {
- return job.getService().flattenToShortString() + " jId=" + job.getId() +
- ", u" + getUserId();
+ StringBuilder sb = new StringBuilder();
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" jId=");
+ sb.append(job.getId());
+ sb.append(" uid=");
+ UserHandle.formatUid(sb, uId);
+ sb.append(' ');
+ sb.append(job.getService().flattenToShortString());
+ return sb.toString();
}
// Dumpsys infrastructure
public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix); UserHandle.formatUid(pw, uId);
+ pw.print(" tag="); pw.println(tag);
pw.print(prefix);
- pw.println(this.toString());
+ pw.print("Source: uid="); UserHandle.formatUid(pw, sourceUid);
+ pw.print(" user="); pw.print(sourceUserId);
+ pw.print(" pkg="); pw.println(sourcePackageName);
+ pw.print(prefix); pw.println("JobInfo:");
+ pw.print(prefix); pw.print(" Service: ");
+ pw.println(job.getService().flattenToShortString());
+ if (job.isPeriodic()) {
+ pw.print(prefix); pw.print(" PERIODIC: interval=");
+ TimeUtils.formatDuration(job.getIntervalMillis(), pw);
+ pw.print(" flex=");
+ TimeUtils.formatDuration(job.getFlexMillis(), pw);
+ pw.println();
+ }
+ if (job.isPersisted()) {
+ pw.print(prefix); pw.println(" PERSISTED");
+ }
+ if (job.getPriority() != 0) {
+ pw.print(prefix); pw.print(" Priority: ");
+ pw.println(job.getPriority());
+ }
+ pw.print(prefix); pw.print(" Requires: charging=");
+ pw.print(job.isRequireCharging());
+ pw.print(" deviceIdle=");
+ pw.println(job.isRequireDeviceIdle());
+ if (job.getTriggerContentUris() != null) {
+ pw.print(prefix); pw.println(" Trigger content URIs:");
+ for (int i=0; i<job.getTriggerContentUris().length; i++) {
+ JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i];
+ pw.print(prefix); pw.print(" ");
+ pw.print(Integer.toHexString(trig.getFlags()));
+ pw.print(' ' );
+ pw.println(trig.getUri());
+ }
+ }
+ if (job.getNetworkType() != JobInfo.NETWORK_TYPE_NONE) {
+ pw.print(prefix); pw.print(" Network type: ");
+ pw.println(job.getNetworkType());
+ }
+ if (job.getMinLatencyMillis() != 0) {
+ pw.print(prefix); pw.print(" Minimum latency: ");
+ TimeUtils.formatDuration(job.getMinLatencyMillis(), pw);
+ pw.println();
+ }
+ if (job.getMaxExecutionDelayMillis() != 0) {
+ pw.print(prefix); pw.print(" Max execution delay: ");
+ TimeUtils.formatDuration(job.getMaxExecutionDelayMillis(), pw);
+ pw.println();
+ }
+ pw.print(prefix); pw.print(" Backoff: policy=");
+ pw.print(job.getBackoffPolicy());
+ pw.print(" initial=");
+ TimeUtils.formatDuration(job.getInitialBackoffMillis(), pw);
+ pw.println();
+ if (job.hasEarlyConstraint()) {
+ pw.print(prefix); pw.println(" Has early constraint");
+ }
+ if (job.hasLateConstraint()) {
+ pw.print(prefix); pw.println(" Has late constraint");
+ }
+ pw.print(prefix); pw.println("Constraints:");
+ if (hasChargingConstraint()) {
+ pw.print(prefix); pw.print(" Charging: ");
+ pw.println(chargingConstraintSatisfied.get());
+ }
+ if (hasTimingDelayConstraint()) {
+ pw.print(prefix); pw.print(" Time delay: ");
+ pw.println(timeDelayConstraintSatisfied.get());
+ }
+ if (hasDeadlineConstraint()) {
+ pw.print(prefix); pw.print(" Deadline: ");
+ pw.println(deadlineConstraintSatisfied.get());
+ }
+ if (hasIdleConstraint()) {
+ pw.print(prefix); pw.print(" System idle: ");
+ pw.println(idleConstraintSatisfied.get());
+ }
+ if (hasUnmeteredConstraint()) {
+ pw.print(prefix); pw.print(" Unmetered: ");
+ pw.println(unmeteredConstraintSatisfied.get());
+ }
+ if (hasConnectivityConstraint()) {
+ pw.print(prefix); pw.print(" Connectivity: ");
+ pw.println(connectivityConstraintSatisfied.get());
+ }
+ if (hasIdleConstraint()) {
+ pw.print(prefix); pw.print(" App not idle: ");
+ pw.println(appNotIdleConstraintSatisfied.get());
+ }
+ if (hasContentTriggerConstraint()) {
+ pw.print(prefix); pw.print(" Content trigger: ");
+ pw.println(contentTriggerConstraintSatisfied.get());
+ }
+ if (changedAuthorities != null) {
+ pw.print(prefix); pw.println("Changed authorities:");
+ for (int i=0; i<changedAuthorities.size(); i++) {
+ pw.print(prefix); pw.print(" "); pw.println(changedAuthorities.valueAt(i));
+ }
+ if (changedUris != null) {
+ pw.print(prefix); pw.println("Changed URIs:");
+ for (int i=0; i<changedUris.size(); i++) {
+ pw.print(prefix); pw.print(" "); pw.println(changedUris.valueAt(i));
+ }
+ }
+ }
+ pw.print(prefix); pw.print("Earliest run time: ");
+ pw.println(formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME));
+ pw.print(prefix); pw.print("Latest run time: ");
+ pw.println(formatRunTime(latestRunTimeElapsedMillis, NO_LATEST_RUNTIME));
+ if (numFailures != 0) {
+ pw.print(prefix); pw.print("Num failures: "); pw.println(numFailures);
+ }
}
}
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 21c30c7a0120..b619ea8cdd5d 100644
--- a/services/core/java/com/android/server/job/controllers/StateController.java
+++ b/services/core/java/com/android/server/job/controllers/StateController.java
@@ -49,11 +49,21 @@ public abstract class StateController {
* Also called when updating a task, so implementing controllers have to be aware of
* preexisting tasks.
*/
- public abstract void maybeStartTrackingJob(JobStatus jobStatus);
+ public abstract void maybeStartTrackingJob(JobStatus jobStatus, JobStatus lastJob);
+ /**
+ * Optionally implement logic here to prepare the job to be executed.
+ */
+ public void prepareForExecution(JobStatus jobStatus) {
+ }
/**
* Remove task - this will happen if the task is cancelled, completed, etc.
*/
- public abstract void maybeStopTrackingJob(JobStatus jobStatus);
+ public abstract void maybeStopTrackingJob(JobStatus jobStatus, boolean forUpdate);
+ /**
+ * Called when a new job is being created to reschedule an old failed job.
+ */
+ public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {
+ }
public abstract void dumpControllerState(PrintWriter pw);
}
diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java
index 854ce3192d45..a68c3adabb08 100644
--- a/services/core/java/com/android/server/job/controllers/TimeController.java
+++ b/services/core/java/com/android/server/job/controllers/TimeController.java
@@ -71,9 +71,9 @@ public class TimeController extends StateController {
* list.
*/
@Override
- public synchronized void maybeStartTrackingJob(JobStatus job) {
+ public synchronized void maybeStartTrackingJob(JobStatus job, JobStatus lastJob) {
if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
- maybeStopTrackingJob(job);
+ maybeStopTrackingJob(job, false);
boolean isInsert = false;
ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
while (it.hasPrevious()) {
@@ -101,7 +101,7 @@ public class TimeController extends StateController {
* Really an == comparison should be enough, but why play with fate? We'll do <=.
*/
@Override
- public synchronized void maybeStopTrackingJob(JobStatus job) {
+ public synchronized void maybeStopTrackingJob(JobStatus job, boolean forUpdate) {
if (mTrackedJobs.remove(job)) {
checkExpiredDelaysAndResetAlarm();
checkExpiredDeadlinesAndResetAlarm();