/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package android.app.job; import static android.util.TimeUtils.formatDuration; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.content.ClipData; import android.content.ComponentName; import android.net.Uri; import android.os.BaseBundle; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; /** * 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 * using the {@link JobInfo.Builder}. * You must specify at least one sort of constraint on the JobInfo object that you are creating. * The goal here is to provide the scheduler with high-level semantics about the work you want to * accomplish. Doing otherwise with throw an exception in your app. */ public class JobInfo implements Parcelable { private static String TAG = "JobInfo"; /** @hide */ @IntDef(prefix = { "NETWORK_TYPE_" }, value = { NETWORK_TYPE_NONE, NETWORK_TYPE_ANY, NETWORK_TYPE_UNMETERED, NETWORK_TYPE_NOT_ROAMING, NETWORK_TYPE_METERED, }) @Retention(RetentionPolicy.SOURCE) public @interface NetworkType {} /** Default. */ public static final int NETWORK_TYPE_NONE = 0; /** This job requires network connectivity. */ public static final int NETWORK_TYPE_ANY = 1; /** This job requires network connectivity that is unmetered. */ public static final int NETWORK_TYPE_UNMETERED = 2; /** This job requires network connectivity that is not roaming. */ public static final int NETWORK_TYPE_NOT_ROAMING = 3; /** This job requires metered connectivity such as most cellular data networks. */ public static final int NETWORK_TYPE_METERED = 4; /** * Amount of backoff a job has initially by default, in milliseconds. */ public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 30000L; // 30 seconds. /** * Maximum backoff we allow for a job, in milliseconds. */ public static final long MAX_BACKOFF_DELAY_MILLIS = 5 * 60 * 60 * 1000; // 5 hours. /** @hide */ @IntDef(prefix = { "BACKOFF_POLICY_" }, value = { BACKOFF_POLICY_LINEAR, BACKOFF_POLICY_EXPONENTIAL, }) @Retention(RetentionPolicy.SOURCE) public @interface BackoffPolicy {} /** * Linearly back-off a failed job. See * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} * retry_time(current_time, num_failures) = * current_time + initial_backoff_millis * num_failures, num_failures >= 1 */ public static final int BACKOFF_POLICY_LINEAR = 0; /** * Exponentially back-off a failed job. See * {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)} * * retry_time(current_time, num_failures) = * current_time + initial_backoff_millis * 2 ^ (num_failures - 1), num_failures >= 1 */ public static final int BACKOFF_POLICY_EXPONENTIAL = 1; /* Minimum interval for a periodic job, in milliseconds. */ private static final long MIN_PERIOD_MILLIS = 15 * 60 * 1000L; // 15 minutes /* Minimum flex for a periodic job, in milliseconds. */ private static final long MIN_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes /** * Minimum backoff interval for a job, in milliseconds * @hide */ public static final long MIN_BACKOFF_MILLIS = 10 * 1000L; // 10 seconds /** * Query the minimum interval allowed for periodic scheduled jobs. Attempting * to declare a smaller period that this when scheduling a job will result in a * job that is still periodic, but will run with this effective period. * * @return The minimum available interval for scheduling periodic jobs, in milliseconds. */ public static final long getMinPeriodMillis() { return MIN_PERIOD_MILLIS; } /** * Query the minimum flex time allowed for periodic scheduled jobs. Attempting * to declare a shorter flex time than this when scheduling such a job will * result in this amount as the effective flex time for the job. * * @return The minimum available flex time for scheduling periodic jobs, in milliseconds. */ public static final long getMinFlexMillis() { return MIN_FLEX_MILLIS; } /** * Query the minimum automatic-reschedule backoff interval permitted for jobs. * @hide */ public static final long getMinBackoffMillis() { return MIN_BACKOFF_MILLIS; } /** * Default type of backoff. * @hide */ public static final int DEFAULT_BACKOFF_POLICY = BACKOFF_POLICY_EXPONENTIAL; /** * Default of {@link #getPriority}. * @hide */ public static final int PRIORITY_DEFAULT = 0; /** * Value of {@link #getPriority} for expedited syncs. * @hide */ public static final int PRIORITY_SYNC_EXPEDITED = 10; /** * Value of {@link #getPriority} for first time initialization syncs. * @hide */ public static final int PRIORITY_SYNC_INITIALIZATION = 20; /** * Value of {@link #getPriority} for a foreground app (overrides the supplied * JobInfo priority if it is smaller). * @hide */ public static final int PRIORITY_FOREGROUND_APP = 30; /** * Value of {@link #getPriority} for the current top app (overrides the supplied * JobInfo priority if it is smaller). * @hide */ public static final int PRIORITY_TOP_APP = 40; /** * Adjustment of {@link #getPriority} if the app has often (50% or more of the time) * been running jobs. * @hide */ public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40; /** * Adjustment of {@link #getPriority} if the app has always (90% or more of the time) * been running jobs. * @hide */ public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80; /** * Indicates that the implementation of this job will be using * {@link JobService#startForeground(int, android.app.Notification)} to run * in the foreground. *
* When set, the internal scheduling of this job will ignore any background * network restrictions for the requesting app. Note that this flag alone * doesn't actually place your {@link JobService} in the foreground; you * still need to post the notification yourself. *
* To use this flag, the caller must hold the
* {@link android.Manifest.permission#CONNECTIVITY_INTERNAL} permission.
*
* @hide
*/
public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0;
/**
* @hide
*/
public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
/**
* @hide
*/
public static final int CONSTRAINT_FLAG_BATTERY_NOT_LOW = 1 << 1;
/**
* @hide
*/
public static final int CONSTRAINT_FLAG_DEVICE_IDLE = 1 << 2;
/**
* @hide
*/
public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3;
private final int jobId;
private final PersistableBundle extras;
private final Bundle transientExtras;
private final ClipData clipData;
private final int clipGrantFlags;
private final ComponentName service;
private final int constraintFlags;
private final TriggerContentUri[] triggerContentUris;
private final long triggerContentUpdateDelay;
private final long triggerContentMaxDelay;
private final boolean hasEarlyConstraint;
private final boolean hasLateConstraint;
private final int networkType;
private final long minLatencyMillis;
private final long maxExecutionDelayMillis;
private final boolean isPeriodic;
private final boolean isPersisted;
private final long intervalMillis;
private final long flexMillis;
private final long initialBackoffMillis;
private final int backoffPolicy;
private final int priority;
private final int flags;
/**
* Unique job id associated with this application (uid). This is the same job ID
* you supplied in the {@link Builder} constructor.
*/
public int getId() {
return jobId;
}
/**
* Bundle of extras which are returned to your application at execution time.
*/
public @NonNull PersistableBundle getExtras() {
return extras;
}
/**
* Bundle of transient extras which are returned to your application at execution time,
* but not persisted by the system.
*/
public @NonNull Bundle getTransientExtras() {
return transientExtras;
}
/**
* ClipData of information that is returned to your application at execution time,
* but not persisted by the system.
*/
public @Nullable ClipData getClipData() {
return clipData;
}
/**
* Permission grants that go along with {@link #getClipData}.
*/
public int getClipGrantFlags() {
return clipGrantFlags;
}
/**
* Name of the service endpoint that will be called back into by the JobScheduler.
*/
public @NonNull ComponentName getService() {
return service;
}
/** @hide */
public int getPriority() {
return priority;
}
/** @hide */
public int getFlags() {
return flags;
}
/**
* Whether this job needs the device to be plugged in.
*/
public boolean isRequireCharging() {
return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0;
}
/**
* Whether this job needs the device's battery level to not be at below the critical threshold.
*/
public boolean isRequireBatteryNotLow() {
return (constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0;
}
/**
* Whether this job needs the device to be in an Idle maintenance window.
*/
public boolean isRequireDeviceIdle() {
return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0;
}
/**
* Whether this job needs the device's storage to not be low.
*/
public boolean isRequireStorageNotLow() {
return (constraintFlags & CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0;
}
/**
* @hide
*/
public int getConstraintFlags() {
return constraintFlags;
}
/**
* Which content: URIs must change for the job to be scheduled. Returns null
* if there are none required.
*/
public @Nullable TriggerContentUri[] getTriggerContentUris() {
return triggerContentUris;
}
/**
* When triggering on content URI changes, this is the delay from when a change
* is detected until the job is scheduled.
*/
public long getTriggerContentUpdateDelay() {
return triggerContentUpdateDelay;
}
/**
* When triggering on content URI changes, this is the maximum delay we will
* use before scheduling the job.
*/
public long getTriggerContentMaxDelay() {
return triggerContentMaxDelay;
}
/**
* The kind of connectivity requirements that the job has.
*/
public @NetworkType int getNetworkType() {
return networkType;
}
/**
* Set for a job that does not recur periodically, to specify a delay after which the job
* will be eligible for execution. This value is not set if the job recurs periodically.
*/
public long getMinLatencyMillis() {
return minLatencyMillis;
}
/**
* See {@link Builder#setOverrideDeadline(long)}. This value is not set if the job recurs
* periodically.
*/
public long getMaxExecutionDelayMillis() {
return maxExecutionDelayMillis;
}
/**
* Track whether this job will repeat with a given period.
*/
public boolean isPeriodic() {
return isPeriodic;
}
/**
* @return Whether or not this job should be persisted across device reboots.
*/
public boolean isPersisted() {
return isPersisted;
}
/**
* Set to the interval between occurrences of this job. This value is not set if the
* job does not recur periodically.
*/
public long getIntervalMillis() {
final long minInterval = getMinPeriodMillis();
return intervalMillis >= minInterval ? intervalMillis : minInterval;
}
/**
* Flex time for this job. Only valid if this is a periodic job. The job can
* execute at any time in a window of flex length at the end of the period.
*/
public long getFlexMillis() {
long interval = getIntervalMillis();
long percentClamp = 5 * interval / 100;
long clampedFlex = Math.max(flexMillis, Math.max(percentClamp, getMinFlexMillis()));
return clampedFlex <= interval ? clampedFlex : interval;
}
/**
* The amount of time the JobScheduler will wait before rescheduling a failed job. This value
* will be increased depending on the backoff policy specified at job creation time. Defaults
* to 30 seconds, minimum is currently 10 seconds.
*/
public long getInitialBackoffMillis() {
final long minBackoff = getMinBackoffMillis();
return initialBackoffMillis >= minBackoff ? initialBackoffMillis : minBackoff;
}
/**
* Return the backoff policy of this job.
*/
public @BackoffPolicy int getBackoffPolicy() {
return backoffPolicy;
}
/**
* User can specify an early constraint of 0L, which is valid, so we keep track of whether the
* function was called at all.
* @hide
*/
public boolean hasEarlyConstraint() {
return hasEarlyConstraint;
}
/**
* User can specify a late constraint of 0L, which is valid, so we keep track of whether the
* function was called at all.
* @hide
*/
public boolean hasLateConstraint() {
return hasLateConstraint;
}
private static boolean kindofEqualsBundle(BaseBundle a, BaseBundle b) {
return (a == b) || (a != null && a.kindofEquals(b));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof JobInfo)) {
return false;
}
JobInfo j = (JobInfo) o;
if (jobId != j.jobId) {
return false;
}
// XXX won't be correct if one is parcelled and the other not.
if (!kindofEqualsBundle(extras, j.extras)) {
return false;
}
// XXX won't be correct if one is parcelled and the other not.
if (!kindofEqualsBundle(transientExtras, j.transientExtras)) {
return false;
}
// XXX for now we consider two different clip data objects to be different,
// regardless of whether their contents are the same.
if (clipData != j.clipData) {
return false;
}
if (clipGrantFlags != j.clipGrantFlags) {
return false;
}
if (!Objects.equals(service, j.service)) {
return false;
}
if (constraintFlags != j.constraintFlags) {
return false;
}
if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) {
return false;
}
if (triggerContentUpdateDelay != j.triggerContentUpdateDelay) {
return false;
}
if (triggerContentMaxDelay != j.triggerContentMaxDelay) {
return false;
}
if (hasEarlyConstraint != j.hasEarlyConstraint) {
return false;
}
if (hasLateConstraint != j.hasLateConstraint) {
return false;
}
if (networkType != j.networkType) {
return false;
}
if (minLatencyMillis != j.minLatencyMillis) {
return false;
}
if (maxExecutionDelayMillis != j.maxExecutionDelayMillis) {
return false;
}
if (isPeriodic != j.isPeriodic) {
return false;
}
if (isPersisted != j.isPersisted) {
return false;
}
if (intervalMillis != j.intervalMillis) {
return false;
}
if (flexMillis != j.flexMillis) {
return false;
}
if (initialBackoffMillis != j.initialBackoffMillis) {
return false;
}
if (backoffPolicy != j.backoffPolicy) {
return false;
}
if (priority != j.priority) {
return false;
}
if (flags != j.flags) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hashCode = jobId;
if (extras != null) {
hashCode = 31 * hashCode + extras.hashCode();
}
if (transientExtras != null) {
hashCode = 31 * hashCode + transientExtras.hashCode();
}
if (clipData != null) {
hashCode = 31 * hashCode + clipData.hashCode();
}
hashCode = 31*hashCode + clipGrantFlags;
if (service != null) {
hashCode = 31 * hashCode + service.hashCode();
}
hashCode = 31 * hashCode + constraintFlags;
if (triggerContentUris != null) {
hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris);
}
hashCode = 31 * hashCode + Long.hashCode(triggerContentUpdateDelay);
hashCode = 31 * hashCode + Long.hashCode(triggerContentMaxDelay);
hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint);
hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint);
hashCode = 31 * hashCode + networkType;
hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis);
hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis);
hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic);
hashCode = 31 * hashCode + Boolean.hashCode(isPersisted);
hashCode = 31 * hashCode + Long.hashCode(intervalMillis);
hashCode = 31 * hashCode + Long.hashCode(flexMillis);
hashCode = 31 * hashCode + Long.hashCode(initialBackoffMillis);
hashCode = 31 * hashCode + backoffPolicy;
hashCode = 31 * hashCode + priority;
hashCode = 31 * hashCode + flags;
return hashCode;
}
private JobInfo(Parcel in) {
jobId = in.readInt();
extras = in.readPersistableBundle();
transientExtras = in.readBundle();
if (in.readInt() != 0) {
clipData = ClipData.CREATOR.createFromParcel(in);
clipGrantFlags = in.readInt();
} else {
clipData = null;
clipGrantFlags = 0;
}
service = in.readParcelable(null);
constraintFlags = in.readInt();
triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
triggerContentUpdateDelay = in.readLong();
triggerContentMaxDelay = in.readLong();
networkType = in.readInt();
minLatencyMillis = in.readLong();
maxExecutionDelayMillis = in.readLong();
isPeriodic = in.readInt() == 1;
isPersisted = in.readInt() == 1;
intervalMillis = in.readLong();
flexMillis = in.readLong();
initialBackoffMillis = in.readLong();
backoffPolicy = in.readInt();
hasEarlyConstraint = in.readInt() == 1;
hasLateConstraint = in.readInt() == 1;
priority = in.readInt();
flags = in.readInt();
}
private JobInfo(JobInfo.Builder b) {
jobId = b.mJobId;
extras = b.mExtras.deepCopy();
transientExtras = b.mTransientExtras.deepCopy();
clipData = b.mClipData;
clipGrantFlags = b.mClipGrantFlags;
service = b.mJobService;
constraintFlags = b.mConstraintFlags;
triggerContentUris = b.mTriggerContentUris != null
? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()])
: null;
triggerContentUpdateDelay = b.mTriggerContentUpdateDelay;
triggerContentMaxDelay = b.mTriggerContentMaxDelay;
networkType = b.mNetworkType;
minLatencyMillis = b.mMinLatencyMillis;
maxExecutionDelayMillis = b.mMaxExecutionDelayMillis;
isPeriodic = b.mIsPeriodic;
isPersisted = b.mIsPersisted;
intervalMillis = b.mIntervalMillis;
flexMillis = b.mFlexMillis;
initialBackoffMillis = b.mInitialBackoffMillis;
backoffPolicy = b.mBackoffPolicy;
hasEarlyConstraint = b.mHasEarlyConstraint;
hasLateConstraint = b.mHasLateConstraint;
priority = b.mPriority;
flags = b.mFlags;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(jobId);
out.writePersistableBundle(extras);
out.writeBundle(transientExtras);
if (clipData != null) {
out.writeInt(1);
clipData.writeToParcel(out, flags);
out.writeInt(clipGrantFlags);
} else {
out.writeInt(0);
}
out.writeParcelable(service, flags);
out.writeInt(constraintFlags);
out.writeTypedArray(triggerContentUris, flags);
out.writeLong(triggerContentUpdateDelay);
out.writeLong(triggerContentMaxDelay);
out.writeInt(networkType);
out.writeLong(minLatencyMillis);
out.writeLong(maxExecutionDelayMillis);
out.writeInt(isPeriodic ? 1 : 0);
out.writeInt(isPersisted ? 1 : 0);
out.writeLong(intervalMillis);
out.writeLong(flexMillis);
out.writeLong(initialBackoffMillis);
out.writeInt(backoffPolicy);
out.writeInt(hasEarlyConstraint ? 1 : 0);
out.writeInt(hasLateConstraint ? 1 : 0);
out.writeInt(priority);
out.writeInt(this.flags);
}
public static final Creator Because setting this property is not compatible with persisted
* jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
* {@link android.app.job.JobInfo.Builder#build()} is called. The main purpose of providing a ClipData is to allow granting of
* URI permissions for data associated with the clip. The exact kind
* of permission grant to perform is specified through grantFlags.
*
* If the ClipData contains items that are Intents, any
* grant flags in those Intents will be ignored. Only flags provided as an argument
* to this method are respected, and will be applied to all Uri or
* Intent items in the clip (or sub-items of the clip).
*
* Because setting this property is not compatible with persisted
* jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
* {@link android.app.job.JobInfo.Builder#build()} is called. Idle mode is a loose definition provided by the system, which means that the device
* is not in use, and has not been in use for some time. As such, it is a good time to
* perform resource heavy jobs. Bear in mind that battery usage will still be attributed
* to your application, and surfaced to the user in battery stats. 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.
* Following this pattern will ensure you do not lost any content changes: while your
* job is running, the system will continue monitoring for content changes, and propagate
* any it sees over to the next job you schedule. Because setting this property is not compatible with periodic or
* persisted jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
* {@link android.app.job.JobInfo.Builder#build()} is called. The following example shows how this feature can be used to monitor for changes
* in the photos on a device.