diff options
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(); |