/*
 * 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.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.util.TimeUtils.formatDuration;

import android.annotation.BytesLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.Overridable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ClipData;
import android.content.ComponentName;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.Uri;
import android.os.BaseBundle;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.Trace;
import android.util.ArraySet;
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.Collections;
import java.util.Objects;
import java.util.Set;

/**
 * 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}.
 * The goal here is to provide the scheduler with high-level semantics about the work you want to
 * accomplish.
 * <p> Prior to Android version {@link Build.VERSION_CODES#Q}, you had to specify at least one
 * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an
 * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is
 * valid to schedule jobs with no constraints.
 */
public class JobInfo implements Parcelable {
    private static String TAG = "JobInfo";

    /**
     * Disallow setting a deadline (via {@link Builder#setOverrideDeadline(long)}) for prefetch
     * jobs ({@link Builder#setPrefetch(boolean)}. Prefetch jobs are meant to run close to the next
     * app launch, so there's no good reason to allow them to have deadlines.
     *
     * We don't drop or cancel any previously scheduled prefetch jobs with a deadline.
     * There's no way for an app to keep a perpetually scheduled prefetch job with a deadline.
     * Prefetch jobs with a deadline will run and apps under this restriction won't be able to
     * schedule new prefetch jobs with a deadline. If a job is rescheduled (by providing
     * {@code true} via {@link JobService#jobFinished(JobParameters, boolean)} or
     * {@link JobService#onStopJob(JobParameters)}'s return value),the deadline is dropped.
     * Periodic jobs require all constraints to be met, so there's no issue with their deadlines.
     *
     * @hide
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    public static final long DISALLOW_DEADLINES_FOR_PREFETCH_JOBS = 194532703L;

    /**
     * Whether to throw an exception when an app provides an invalid priority value via
     * {@link Builder#setPriority(int)}. Legacy apps may be incorrectly using the API and
     * so the call will silently fail for them if they continue using the API.
     *
     * @hide
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    public static final long THROW_ON_INVALID_PRIORITY_VALUE = 140852299L;

    /**
     * Require that estimated network bytes are nonnegative.
     *
     * @hide
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    public static final long REJECT_NEGATIVE_NETWORK_ESTIMATES = 253665015L;

    /**
     * Enforce a minimum time window between job latencies and deadlines.
     *
     * @hide
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    @Overridable // Aid in testing
    public static final long ENFORCE_MINIMUM_TIME_WINDOWS = 311402873L;

    /**
     * Require that minimum latencies and override deadlines are nonnegative.
     *
     * @hide
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    public static final long REJECT_NEGATIVE_DELAYS_AND_DEADLINES = 323349338L;

    /** @hide */
    @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
            NETWORK_TYPE_NONE,
            NETWORK_TYPE_ANY,
            NETWORK_TYPE_UNMETERED,
            NETWORK_TYPE_NOT_ROAMING,
            NETWORK_TYPE_CELLULAR,
    })
    @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 network connectivity that is a cellular network. */
    public static final int NETWORK_TYPE_CELLULAR = 4;

    /**
     * This job requires metered connectivity such as most cellular data
     * networks.
     *
     * @deprecated Cellular networks may be unmetered, or Wi-Fi networks may be
     *             metered, so this isn't a good way of selecting a specific
     *             transport. Instead, use {@link #NETWORK_TYPE_CELLULAR} or
     *             {@link android.net.NetworkRequest.Builder#addTransportType(int)}
     *             if your job requires a specific network transport.
     */
    @Deprecated
    public static final int NETWORK_TYPE_METERED = NETWORK_TYPE_CELLULAR;

    /** Sentinel value indicating that bytes are unknown. */
    public static final int NETWORK_BYTES_UNKNOWN = -1;

    /**
     * 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

    private static final long MIN_ALLOWED_TIME_WINDOW_MILLIS = MIN_PERIOD_MILLIS;

    /**
     * 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 than 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;

    /**
     * Job has minimal value to the user. The user has absolutely no expectation
     * or knowledge of this task and it has no bearing on the user's perception of
     * the app whatsoever. JobScheduler <i>may</i> decide to defer these tasks while
     * there are higher priority tasks in order to ensure there is sufficient quota
     * available for the higher priority tasks.
     * A sample task of min priority: uploading analytics
     */
    public static final int PRIORITY_MIN = 100;

    /**
     * Low priority. The task provides some benefit to users, but is not critical
     * and is more of a nice-to-have. This is more important than minimum priority
     * jobs and will be prioritized ahead of them, but may still be deferred in lieu
     * of higher priority jobs. JobScheduler <i>may</i> decide to defer these tasks
     * while there are higher priority tasks in order to ensure there is sufficient
     * quota available for the higher priority tasks.
     * A sample task of low priority: prefetching data the user hasn't requested
     */
    public static final int PRIORITY_LOW = 200;

    /**
     * Default value for all regular jobs. As noted in {@link JobScheduler},
     * these jobs have a general execution time of 10 minutes.
     * Receives the standard job management policy.
     */
    public static final int PRIORITY_DEFAULT = 300;

    /**
     * This task should be ordered ahead of most other tasks. It may be
     * deferred a little, but if it doesn't run at some point, the user may think
     * something is wrong. Assuming all constraints remain satisfied
     * (including ideal system load conditions), these jobs can have an
     * execution time of at least 4 minutes. Setting all of your jobs to high
     * priority will not be beneficial to your app and in fact may hurt its
     * performance in the long run.
     */
    public static final int PRIORITY_HIGH = 400;

    /**
     * This task is critical to user experience or functionality
     * and should be run ahead of all other tasks. Only
     * {@link Builder#setExpedited(boolean) expedited jobs} and
     * {@link Builder#setUserInitiated(boolean) user-initiated jobs} can have this priority.
     * <p>
     * Example tasks of max priority:
     * <ul>
     *     <li>Receiving a text message and processing it to show a notification</li>
     *     <li>Downloading or uploading some content the user requested to transfer immediately</li>
     * </ul>
     */
    public static final int PRIORITY_MAX = 500;

    /** @hide */
    @IntDef(prefix = {"PRIORITY_"}, value = {
            PRIORITY_MIN,
            PRIORITY_LOW,
            PRIORITY_DEFAULT,
            PRIORITY_HIGH,
            PRIORITY_MAX,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Priority {
    }

    /**
     * Default of {@link #getBias}.
     * @hide
     */
    public static final int BIAS_DEFAULT = 0;

    /**
     * Value of {@link #getBias} for expedited syncs.
     * @hide
     */
    public static final int BIAS_SYNC_EXPEDITED = 10;

    /**
     * Value of {@link #getBias} for first time initialization syncs.
     * @hide
     */
    public static final int BIAS_SYNC_INITIALIZATION = 20;

    /**
     * Value of {@link #getBias} for a BFGS app (overrides the supplied
     * JobInfo bias if it is smaller).
     * @hide
     */
    public static final int BIAS_BOUND_FOREGROUND_SERVICE = 30;

    /** @hide For backward compatibility. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static final int PRIORITY_FOREGROUND_APP = BIAS_BOUND_FOREGROUND_SERVICE;

    /**
     * Value of {@link #getBias} for a FG service app (overrides the supplied
     * JobInfo bias if it is smaller).
     * @hide
     */
    public static final int BIAS_FOREGROUND_SERVICE = 35;

    /** @hide For backward compatibility. */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static final int PRIORITY_FOREGROUND_SERVICE = BIAS_FOREGROUND_SERVICE;

    /**
     * Value of {@link #getBias} for the current top app (overrides the supplied
     * JobInfo bias if it is smaller).
     * @hide
     */
    public static final int BIAS_TOP_APP = 40;

    /**
     * Adjustment of {@link #getBias} if the app has often (50% or more of the time)
     * been running jobs.
     * @hide
     */
    public static final int BIAS_ADJ_OFTEN_RUNNING = -40;

    /**
     * Adjustment of {@link #getBias} if the app has always (90% or more of the time)
     * been running jobs.
     * @hide
     */
    public static final int BIAS_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.
     * <p>
     * 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.
     * <p>
     * To use this flag, the caller must hold the
     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL} permission.
     *
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public static final int FLAG_WILL_BE_FOREGROUND = 1 << 0;

    /**
     * Allows this job to run despite doze restrictions as long as the app is in the foreground
     * or on the temporary allowlist
     * @hide
     */
    public static final int FLAG_IMPORTANT_WHILE_FOREGROUND = 1 << 1;

    /**
     * @hide
     */
    public static final int FLAG_PREFETCH = 1 << 2;

    /**
     * This job needs to be exempted from the app standby throttling. Only the system (UID 1000)
     * can set it. Jobs with a time constraint must not have it.
     *
     * @hide
     */
    public static final int FLAG_EXEMPT_FROM_APP_STANDBY = 1 << 3;

    /**
     * Whether it's an expedited job or not.
     *
     * @hide
     */
    public static final int FLAG_EXPEDITED = 1 << 4;

    /**
     * Whether it's a user initiated job or not.
     *
     * @hide
     */
    public static final int FLAG_USER_INITIATED = 1 << 5;

    /**
     * @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;

    /** @hide */
    public static final int MAX_NUM_DEBUG_TAGS = 32;

    /** @hide */
    public static final int MAX_DEBUG_TAG_LENGTH = 127;

    /** @hide */
    public static final int MAX_TRACE_TAG_LENGTH = Trace.MAX_SECTION_NAME_LEN;

    @UnsupportedAppUsage
    private final int jobId;
    private final PersistableBundle extras;
    private final Bundle transientExtras;
    private final ClipData clipData;
    private final int clipGrantFlags;
    @UnsupportedAppUsage
    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 NetworkRequest networkRequest;
    private final long networkDownloadBytes;
    private final long networkUploadBytes;
    private final long minimumNetworkChunkBytes;
    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 mBias;
    @Priority
    private final int mPriority;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private final int flags;
    private final ArraySet<String> mDebugTags;
    @Nullable
    private final String mTraceTag;

    /**
     * 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;
    }

    /**
     * @see JobInfo.Builder#setExtras(PersistableBundle)
     */
    public @NonNull PersistableBundle getExtras() {
        return extras;
    }

    /**
     * @see JobInfo.Builder#setTransientExtras(Bundle)
     */
    public @NonNull Bundle getTransientExtras() {
        return transientExtras;
    }

    /**
     * @see JobInfo.Builder#setClipData(ClipData, int)
     */
    public @Nullable ClipData getClipData() {
        return clipData;
    }

    /**
     * @see JobInfo.Builder#setClipData(ClipData, int)
     */
    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 getBias() {
        return mBias;
    }

    /**
     * @see JobInfo.Builder#setPriority(int)
     */
    @Priority
    public int getPriority() {
        return mPriority;
    }

    /** @hide */
    public int getFlags() {
        return flags;
    }

    /** @hide */
    public boolean isExemptedFromAppStandby() {
        return ((flags & FLAG_EXEMPT_FROM_APP_STANDBY) != 0) && !isPeriodic();
    }

    /**
     * @see JobInfo.Builder#setRequiresCharging(boolean)
     */
    public boolean isRequireCharging() {
        return (constraintFlags & CONSTRAINT_FLAG_CHARGING) != 0;
    }

    /**
     * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean)
     */
    public boolean isRequireBatteryNotLow() {
        return (constraintFlags & CONSTRAINT_FLAG_BATTERY_NOT_LOW) != 0;
    }

    /**
     * @see JobInfo.Builder#setRequiresDeviceIdle(boolean)
     */
    public boolean isRequireDeviceIdle() {
        return (constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0;
    }

    /**
     * @see JobInfo.Builder#setRequiresStorageNotLow(boolean)
     */
    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.
     * @see JobInfo.Builder#addTriggerContentUri(TriggerContentUri)
     */
    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.
     * @see JobInfo.Builder#setTriggerContentUpdateDelay(long)
     */
    public long getTriggerContentUpdateDelay() {
        return triggerContentUpdateDelay;
    }

    /**
     * When triggering on content URI changes, this is the maximum delay we will
     * use before scheduling the job.
     * @see JobInfo.Builder#setTriggerContentMaxDelay(long)
     */
    public long getTriggerContentMaxDelay() {
        return triggerContentMaxDelay;
    }

    /**
     * Return the basic description of the kind of network this job requires.
     *
     * @deprecated This method attempts to map {@link #getRequiredNetwork()}
     *             into the set of simple constants, which results in a loss of
     *             fidelity. Callers should move to using
     *             {@link #getRequiredNetwork()} directly.
     * @see Builder#setRequiredNetworkType(int)
     */
    @Deprecated
    public @NetworkType int getNetworkType() {
        if (networkRequest == null) {
            return NETWORK_TYPE_NONE;
        } else if (networkRequest.hasCapability(NET_CAPABILITY_NOT_METERED)) {
            return NETWORK_TYPE_UNMETERED;
        } else if (networkRequest.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
            return NETWORK_TYPE_NOT_ROAMING;
        } else if (networkRequest.hasTransport(TRANSPORT_CELLULAR)) {
            return NETWORK_TYPE_CELLULAR;
        } else {
            return NETWORK_TYPE_ANY;
        }
    }

    /**
     * Return the detailed description of the kind of network this job requires,
     * or {@code null} if no specific kind of network is required.
     *
     * @see Builder#setRequiredNetwork(NetworkRequest)
     */
    public @Nullable NetworkRequest getRequiredNetwork() {
        return networkRequest;
    }

    /**
     * Return the estimated size of download traffic that will be performed by
     * this job, in bytes.
     *
     * @return Estimated size of download traffic, or
     *         {@link #NETWORK_BYTES_UNKNOWN} when unknown.
     * @see Builder#setEstimatedNetworkBytes(long, long)
     */
    public @BytesLong long getEstimatedNetworkDownloadBytes() {
        return networkDownloadBytes;
    }

    /**
     * Return the estimated size of upload traffic that will be performed by
     * this job, in bytes.
     *
     * @return Estimated size of upload traffic, or
     *         {@link #NETWORK_BYTES_UNKNOWN} when unknown.
     * @see Builder#setEstimatedNetworkBytes(long, long)
     */
    public @BytesLong long getEstimatedNetworkUploadBytes() {
        return networkUploadBytes;
    }

    /**
     * Return the smallest piece of data that cannot be easily paused and resumed, in bytes.
     *
     * @return Smallest piece of data that cannot be easily paused and resumed, or
     *         {@link #NETWORK_BYTES_UNKNOWN} when unknown.
     * @see Builder#setMinimumNetworkChunkBytes(long)
     */
    public @BytesLong long getMinimumNetworkChunkBytes() {
        return minimumNetworkChunkBytes;
    }

    /**
     * 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.
     * @see JobInfo.Builder#setMinimumLatency(long)
     */
    public long getMinLatencyMillis() {
        return Math.max(0, minLatencyMillis);
    }

    /**
     * @see JobInfo.Builder#setOverrideDeadline(long)
     */
    public long getMaxExecutionDelayMillis() {
        return Math.max(0, maxExecutionDelayMillis);
    }

    /**
     * Track whether this job will repeat with a given period.
     * @see JobInfo.Builder#setPeriodic(long)
     * @see JobInfo.Builder#setPeriodic(long, long)
     */
    public boolean isPeriodic() {
        return isPeriodic;
    }

    /**
     * @see JobInfo.Builder#setPersisted(boolean)
     */
    public boolean isPersisted() {
        return isPersisted;
    }

    /**
     * Set to the interval between occurrences of this job. This value is <b>not</b> set if the
     * job does not recur periodically.
     * @see JobInfo.Builder#setPeriodic(long)
     * @see JobInfo.Builder#setPeriodic(long, long)
     */
    public long getIntervalMillis() {
        return intervalMillis;
    }

    /**
     * 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.
     * @see JobInfo.Builder#setPeriodic(long)
     * @see JobInfo.Builder#setPeriodic(long, long)
     */
    public long getFlexMillis() {
        return flexMillis;
    }

    /**
     * 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.
     * @see JobInfo.Builder#setBackoffCriteria(long, int)
     */
    public long getInitialBackoffMillis() {
        return initialBackoffMillis;
    }

    /**
     * Return the backoff policy of this job.
     *
     * @see JobInfo.Builder#setBackoffCriteria(long, int)
     */
    public @BackoffPolicy int getBackoffPolicy() {
        return backoffPolicy;
    }

    /**
     * @see JobInfo.Builder#addDebugTag(String)
     */
    @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
    @NonNull
    public Set<String> getDebugTags() {
        return Collections.unmodifiableSet(mDebugTags);
    }

    /**
     * @see JobInfo.Builder#addDebugTag(String)
     * @hide
     */
    @NonNull
    public ArraySet<String> getDebugTagsArraySet() {
        return mDebugTags;
    }

    /**
     * @see JobInfo.Builder#setTraceTag(String)
     */
    @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
    @Nullable
    public String getTraceTag() {
        return mTraceTag;
    }

    /**
     * @see JobInfo.Builder#setExpedited(boolean)
     */
    public boolean isExpedited() {
        return (flags & FLAG_EXPEDITED) != 0;
    }

    /**
     * @see JobInfo.Builder#setUserInitiated(boolean)
     */
    public boolean isUserInitiated() {
        return (flags & FLAG_USER_INITIATED) != 0;
    }

    /**
     * @see JobInfo.Builder#setImportantWhileForeground(boolean)
     */
    public boolean isImportantWhileForeground() {
        return (flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0;
    }

    /**
     * @see JobInfo.Builder#setPrefetch(boolean)
     */
    public boolean isPrefetch() {
        return (flags & FLAG_PREFETCH) != 0;
    }

    /**
     * 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;
    }

    @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 (!BaseBundle.kindofEquals(extras, j.extras)) {
            return false;
        }
        // XXX won't be correct if one is parcelled and the other not.
        if (!BaseBundle.kindofEquals(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 (!Objects.equals(networkRequest, j.networkRequest)) {
            return false;
        }
        if (networkDownloadBytes != j.networkDownloadBytes) {
            return false;
        }
        if (networkUploadBytes != j.networkUploadBytes) {
            return false;
        }
        if (minimumNetworkChunkBytes != j.minimumNetworkChunkBytes) {
            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 (mBias != j.mBias) {
            return false;
        }
        if (mPriority != j.mPriority) {
            return false;
        }
        if (flags != j.flags) {
            return false;
        }
        if (!mDebugTags.equals(j.mDebugTags)) {
            return false;
        }
        if (!Objects.equals(mTraceTag, j.mTraceTag)) {
            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);
        if (networkRequest != null) {
            hashCode = 31 * hashCode + networkRequest.hashCode();
        }
        hashCode = 31 * hashCode + Long.hashCode(networkDownloadBytes);
        hashCode = 31 * hashCode + Long.hashCode(networkUploadBytes);
        hashCode = 31 * hashCode + Long.hashCode(minimumNetworkChunkBytes);
        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 + mBias;
        hashCode = 31 * hashCode + mPriority;
        hashCode = 31 * hashCode + flags;
        if (mDebugTags.size() > 0) {
            hashCode = 31 * hashCode + mDebugTags.hashCode();
        }
        if (mTraceTag != null) {
            hashCode = 31 * hashCode + mTraceTag.hashCode();
        }
        return hashCode;
    }

    @SuppressWarnings("UnsafeParcelApi")
    private JobInfo(Parcel in) {
        jobId = in.readInt();
        final PersistableBundle persistableExtras = in.readPersistableBundle();
        extras = persistableExtras != null ? persistableExtras : PersistableBundle.EMPTY;
        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();
        if (in.readInt() != 0) {
            networkRequest = NetworkRequest.CREATOR.createFromParcel(in);
        } else {
            networkRequest = null;
        }
        networkDownloadBytes = in.readLong();
        networkUploadBytes = in.readLong();
        minimumNetworkChunkBytes = in.readLong();
        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;
        mBias = in.readInt();
        mPriority = in.readInt();
        flags = in.readInt();
        final int numDebugTags = in.readInt();
        mDebugTags = new ArraySet<>();
        for (int i = 0; i < numDebugTags; ++i) {
            final String tag = in.readString();
            if (tag == null) {
                throw new IllegalStateException("malformed parcel");
            }
            mDebugTags.add(tag.intern());
        }
        final String traceTag = in.readString();
        mTraceTag = traceTag == null ? null : traceTag.intern();
    }

    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;
        networkRequest = b.mNetworkRequest;
        networkDownloadBytes = b.mNetworkDownloadBytes;
        networkUploadBytes = b.mNetworkUploadBytes;
        minimumNetworkChunkBytes = b.mMinimumNetworkChunkBytes;
        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;
        mBias = b.mBias;
        mPriority = b.mPriority;
        flags = b.mFlags;
        mDebugTags = b.mDebugTags;
        mTraceTag = b.mTraceTag;
    }

    @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);
        if (networkRequest != null) {
            out.writeInt(1);
            networkRequest.writeToParcel(out, flags);
        } else {
            out.writeInt(0);
        }
        out.writeLong(networkDownloadBytes);
        out.writeLong(networkUploadBytes);
        out.writeLong(minimumNetworkChunkBytes);
        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(mBias);
        out.writeInt(mPriority);
        out.writeInt(this.flags);
        // Explicitly write out values here to avoid double looping to intern the strings
        // when unparcelling.
        final int numDebugTags = mDebugTags.size();
        out.writeInt(numDebugTags);
        for (int i = 0; i < numDebugTags; ++i) {
            out.writeString(mDebugTags.valueAt(i));
        }
        out.writeString(mTraceTag);
    }

    public static final @android.annotation.NonNull Creator<JobInfo> CREATOR = new Creator<JobInfo>() {
        @Override
        public JobInfo createFromParcel(Parcel in) {
            return new JobInfo(in);
        }

        @Override
        public JobInfo[] newArray(int size) {
            return new JobInfo[size];
        }
    };

    @Override
    public String toString() {
        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;

        /** @hide */
        @Retention(RetentionPolicy.SOURCE)
        @IntDef(flag = true, prefix = { "FLAG_" }, value = {
                FLAG_NOTIFY_FOR_DESCENDANTS,
        })
        public @interface Flags { }

        /**
         * 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 Flags for the observer.
         */
        public TriggerContentUri(@NonNull Uri uri, @Flags int flags) {
            mUri = Objects.requireNonNull(uri);
            mFlags = flags;
        }

        /**
         * Return the Uri this trigger was created for.
         */
        public Uri getUri() {
            return mUri;
        }

        /**
         * Return the flags supplied for the trigger.
         */
        public @Flags int getFlags() {
            return mFlags;
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof TriggerContentUri)) {
                return false;
            }
            TriggerContentUri t = (TriggerContentUri) o;
            return Objects.equals(t.mUri, mUri) && t.mFlags == mFlags;
        }

        @Override
        public int hashCode() {
            return (mUri == null ? 0 : mUri.hashCode()) ^ 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 @android.annotation.NonNull 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 final int mJobId;
        private final ComponentName mJobService;
        private PersistableBundle mExtras = PersistableBundle.EMPTY;
        private Bundle mTransientExtras = Bundle.EMPTY;
        private ClipData mClipData;
        private int mClipGrantFlags;
        private int mBias = BIAS_DEFAULT;
        @Priority
        private int mPriority = PRIORITY_DEFAULT;
        private int mFlags;
        // Requirements.
        private int mConstraintFlags;
        private NetworkRequest mNetworkRequest;
        private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN;
        private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN;
        private long mMinimumNetworkChunkBytes = NETWORK_BYTES_UNKNOWN;
        private ArrayList<TriggerContentUri> mTriggerContentUris;
        private long mTriggerContentUpdateDelay = -1;
        private long mTriggerContentMaxDelay = -1;
        private boolean mIsPersisted;
        // One-off parameters.
        private long mMinLatencyMillis;
        private long mMaxExecutionDelayMillis;
        // Periodic parameters.
        private boolean mIsPeriodic;
        private boolean mHasEarlyConstraint;
        private boolean mHasLateConstraint;
        private long mIntervalMillis;
        private long mFlexMillis;
        // Back-off parameters.
        private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS;
        private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY;
        /** Easy way to track whether the client has tried to set a back-off policy. */
        private boolean mBackoffPolicySet = false;
        private final ArraySet<String> mDebugTags = new ArraySet<>();
        private String mTraceTag;

        /**
         * Initialize a new Builder to construct a {@link JobInfo}.
         *
         * @param jobId Application-provided id for this job. Subsequent calls to cancel, or
         * jobs created with the same jobId, will update the pre-existing job with
         * the same id.  This ID must be unique across all clients of the same uid
         * (not just the same package).  You will want to make sure this is a stable
         * id across app updates, so probably not based on a resource ID.
         * @param jobService The endpoint that you implement that will receive the callback from the
         * JobScheduler.
         */
        public Builder(int jobId, @NonNull ComponentName jobService) {
            mJobService = jobService;
            mJobId = jobId;
        }

        /**
         * Creates a new Builder of JobInfo from an existing instance.
         * @hide
         */
        public Builder(@NonNull JobInfo job) {
            mJobId = job.getId();
            mJobService = job.getService();
            mExtras = job.getExtras();
            mTransientExtras = job.getTransientExtras();
            mClipData = job.getClipData();
            mClipGrantFlags = job.getClipGrantFlags();
            mBias = job.getBias();
            mFlags = job.getFlags();
            mConstraintFlags = job.getConstraintFlags();
            mNetworkRequest = job.getRequiredNetwork();
            mNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes();
            mNetworkUploadBytes = job.getEstimatedNetworkUploadBytes();
            mMinimumNetworkChunkBytes = job.getMinimumNetworkChunkBytes();
            mTriggerContentUris = job.getTriggerContentUris() != null
                    ? new ArrayList<>(Arrays.asList(job.getTriggerContentUris())) : null;
            mTriggerContentUpdateDelay = job.getTriggerContentUpdateDelay();
            mTriggerContentMaxDelay = job.getTriggerContentMaxDelay();
            mIsPersisted = job.isPersisted();
            mMinLatencyMillis = job.getMinLatencyMillis();
            mMaxExecutionDelayMillis = job.getMaxExecutionDelayMillis();
            mIsPeriodic = job.isPeriodic();
            mHasEarlyConstraint = job.hasEarlyConstraint();
            mHasLateConstraint = job.hasLateConstraint();
            mIntervalMillis = job.getIntervalMillis();
            mFlexMillis = job.getFlexMillis();
            mInitialBackoffMillis = job.getInitialBackoffMillis();
            // mBackoffPolicySet isn't set but it's fine since this is copying from an already valid
            // job.
            mBackoffPolicy = job.getBackoffPolicy();
            mPriority = job.getPriority();
        }

        /**
         * Add a debug tag to help track what this job is for. The tags may show in debug dumps
         * or app metrics. Do not put personally identifiable information (PII) in the tag.
         * <p>
         * Tags have the following requirements:
         * <ul>
         *   <li>Tags cannot be more than 127 characters.</li>
         *   <li>
         *       Since leading and trailing whitespace can lead to hard-to-debug issues,
         *       tags should not include leading or trailing whitespace.
         *       All tags will be {@link String#trim() trimmed}.
         *   </li>
         *   <li>An empty String (after trimming) is not allowed.</li>
         *   <li>Should not have personally identifiable information (PII).</li>
         *   <li>A job cannot have more than 32 tags.</li>
         * </ul>
         *
         * @param tag A debug tag that helps describe what the job is for.
         * @return This object for method chaining
         */
        @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
        @NonNull
        public Builder addDebugTag(@NonNull String tag) {
            mDebugTags.add(validateDebugTag(tag));
            return this;
        }

        /** @hide */
        @NonNull
        public void addDebugTags(@NonNull Set<String> tags) {
            mDebugTags.addAll(tags);
        }

        /**
         * Remove a tag set via {@link #addDebugTag(String)}.
         * @param tag The tag to remove
         * @return This object for method chaining
         */
        @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
        @SuppressLint("BuilderSetStyle")
        @NonNull
        public Builder removeDebugTag(@NonNull String tag) {
            mDebugTags.remove(tag);
            return this;
        }

        /** @hide */
        @NonNull
        @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
        public Builder setBias(int bias) {
            mBias = bias;
            return this;
        }

        /**
         * Indicate the priority for this job. The priority set here will be used to sort jobs
         * for the calling app and apply slightly different policies based on the priority.
         * The priority will <b>NOT</b> be used as a global sorting value to sort between
         * different app's jobs. Use this to inform the system about which jobs it should try
         * to run before other jobs. Giving the same priority to all of your jobs will result
         * in them all being treated the same. The priorities each have slightly different
         * behaviors, as noted in their relevant javadoc.
         *
         * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
         * the priority will only affect sorting order within the job's namespace.
         *
         * <b>NOTE:</b> Setting all of your jobs to high priority will not be
         * beneficial to your app and in fact may hurt its performance in the
         * long run.
         *
         * In order to prevent starvation, repeatedly retried jobs (because of failures) will slowly
         * have their priorities lowered.
         *
         * @see JobInfo#getPriority()
         */
        @NonNull
        public Builder setPriority(@Priority int priority) {
            if (priority > PRIORITY_MAX || priority < PRIORITY_MIN) {
                if (Compatibility.isChangeEnabled(THROW_ON_INVALID_PRIORITY_VALUE)) {
                    throw new IllegalArgumentException("Invalid priority value");
                }
                // No-op for invalid calls of apps that are targeting S-. This was an unsupported
                // API before Tiramisu, so anyone calling this that isn't targeting T isn't
                // guaranteed a behavior change.
                return this;
            }
            mPriority = priority;
            return this;
        }

        /** @hide */
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        public Builder setFlags(int flags) {
            mFlags = flags;
            return this;
        }

        /**
         * Set optional extras. This is persisted, so we only allow primitive types.
         * @param extras Bundle containing extras you want the scheduler to hold on to for you.
         * @see JobInfo#getExtras()
         */
        public Builder setExtras(@NonNull PersistableBundle extras) {
            mExtras = extras;
            return this;
        }

        /**
         * Set optional transient extras.
         *
         * <p>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.</p>
         *
         * @param extras Bundle containing extras you want the scheduler to hold on to for you.
         * @see JobInfo#getTransientExtras()
         */
        public Builder setTransientExtras(@NonNull Bundle extras) {
            mTransientExtras = extras;
            return this;
        }

        /**
         * Set a {@link ClipData} associated with this Job.
         *
         * <p>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 <var>grantFlags</var>.
         *
         * <p>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).
         *
         * <p>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.</p>
         *
         * @param clip The new clip to set.  May be null to clear the current clip.
         * @param grantFlags The desired permissions to grant for any URIs.  This should be
         * a combination of {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION},
         * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and
         * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}.
         * @see JobInfo#getClipData()
         * @see JobInfo#getClipGrantFlags()
         */
        public Builder setClipData(@Nullable ClipData clip, int grantFlags) {
            mClipData = clip;
            mClipGrantFlags = grantFlags;
            return this;
        }

        /**
         * Set basic description of the kind of network your job requires. If
         * you need more precise control over network capabilities, see
         * {@link #setRequiredNetwork(NetworkRequest)}.
         * <p>
         * If your job doesn't need a network connection, you don't need to call
         * this method, as the default value is {@link #NETWORK_TYPE_NONE}.
         * <p>
         * Calling this method defines network as a strict requirement for your
         * job. If the network requested is not available your job will never
         * run. See {@link #setOverrideDeadline(long)} to change this behavior.
         * Calling this method will override any requirements previously defined
         * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only
         * want to call one of these methods.
         *
         * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
         * an app must hold the
         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to
         * schedule a job that requires a network.
         *
         * <p> Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
         * {@link JobScheduler} may try to shift the execution of jobs requiring
         * {@link #NETWORK_TYPE_ANY} to when there is access to an un-metered network.
         *
         * <p class="note">
         * When your job executes in
         * {@link JobService#onStartJob(JobParameters)}, be sure to use the
         * specific network returned by {@link JobParameters#getNetwork()},
         * otherwise you'll use the default network which may not meet this
         * constraint.
         *
         * @see #setRequiredNetwork(NetworkRequest)
         * @see JobInfo#getNetworkType()
         * @see JobParameters#getNetwork()
         */
        public Builder setRequiredNetworkType(@NetworkType int networkType) {
            if (networkType == NETWORK_TYPE_NONE) {
                return setRequiredNetwork(null);
            } else {
                final NetworkRequest.Builder builder = new NetworkRequest.Builder();

                // All types require validated Internet
                builder.addCapability(NET_CAPABILITY_INTERNET);
                builder.addCapability(NET_CAPABILITY_VALIDATED);
                builder.removeCapability(NET_CAPABILITY_NOT_VPN);
                builder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);

                if (networkType == NETWORK_TYPE_ANY) {
                    // No other capabilities
                } else if (networkType == NETWORK_TYPE_UNMETERED) {
                    builder.addCapability(NET_CAPABILITY_NOT_METERED);
                } else if (networkType == NETWORK_TYPE_NOT_ROAMING) {
                    builder.addCapability(NET_CAPABILITY_NOT_ROAMING);
                } else if (networkType == NETWORK_TYPE_CELLULAR) {
                    builder.addTransportType(TRANSPORT_CELLULAR);
                }

                return setRequiredNetwork(builder.build());
            }
        }

        /**
         * Set detailed description of the kind of network your job requires.
         * <p>
         * If your job doesn't need a network connection, you don't need to call
         * this method, as the default is {@code null}.
         * <p>
         * Calling this method defines network as a strict requirement for your
         * job. If the network requested is not available your job will never
         * run. See {@link #setOverrideDeadline(long)} to change this behavior.
         * Calling this method will override any requirements previously defined
         * by {@link #setRequiredNetworkType(int)}; you typically only want to
         * call one of these methods.
         * <p class="note">
         * When your job executes in
         * {@link JobService#onStartJob(JobParameters)}, be sure to use the
         * specific network returned by {@link JobParameters#getNetwork()},
         * otherwise you'll use the default network which may not meet this
         * constraint.
         *
         * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
         * an app must hold the
         * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permission to
         * schedule a job that requires a network.
         *
         * @param networkRequest The detailed description of the kind of network
         *            this job requires, or {@code null} if no specific kind of
         *            network is required. Defining a {@link NetworkSpecifier}
         *            is only supported for jobs that aren't persisted.
         * @see #setRequiredNetworkType(int)
         * @see JobInfo#getRequiredNetwork()
         * @see JobParameters#getNetwork()
         */
        public Builder setRequiredNetwork(@Nullable NetworkRequest networkRequest) {
            mNetworkRequest = networkRequest;
            return this;
        }

        /**
         * Set the estimated size of network traffic that will be performed by
         * this job, in bytes.
         * <p>
         * Apps are encouraged to provide values that are as accurate as
         * possible, but when the exact size isn't available, an
         * order-of-magnitude estimate can be provided instead. Here are some
         * specific examples:
         * <ul>
         * <li>A job that is backing up a photo knows the exact size of that
         * photo, so it should provide that size as the estimate.
         * <li>A job that refreshes top news stories wouldn't know an exact
         * size, but if the size is expected to be consistently around 100KB, it
         * can provide that order-of-magnitude value as the estimate.
         * <li>A job that synchronizes email could end up using an extreme range
         * of data, from under 1KB when nothing has changed, to dozens of MB
         * when there are new emails with attachments. Jobs that cannot provide
         * reasonable estimates should use the sentinel value
         * {@link JobInfo#NETWORK_BYTES_UNKNOWN}.
         * </ul>
         * Note that the system may choose to delay jobs with large network
         * usage estimates when the device has a poor network connection, in
         * order to save battery and possible network costs.
         * Starting from Android version {@link Build.VERSION_CODES#S}, JobScheduler may attempt
         * to run large jobs when the device is charging and on an unmetered network, even if the
         * network is slow. This gives large jobs an opportunity to make forward progress, even if
         * they risk timing out.
         * <p>
         * The values provided here only reflect the traffic that will be
         * performed by the base job; if you're using {@link JobWorkItem} then
         * you also need to define the network traffic used by each work item
         * when constructing them.
         *
         * <p class="note">
         * Prior to Android version {@link Build.VERSION_CODES#TIRAMISU}, JobScheduler used the
         * estimated transfer numbers in a similar fashion to
         * {@link #setMinimumNetworkChunkBytes(long)} (to estimate if the work would complete
         * within the time available to job). In other words, JobScheduler treated the transfer as
         * all-or-nothing. Starting from Android version {@link Build.VERSION_CODES#TIRAMISU},
         * JobScheduler will only use the estimated transfer numbers in this manner if minimum
         * chunk sizes have not been provided via {@link #setMinimumNetworkChunkBytes(long)}.
         *
         * @param downloadBytes The estimated size of network traffic that will
         *            be downloaded by this job, in bytes.
         * @param uploadBytes The estimated size of network traffic that will be
         *            uploaded by this job, in bytes.
         * @see JobInfo#getEstimatedNetworkDownloadBytes()
         * @see JobInfo#getEstimatedNetworkUploadBytes()
         * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long)
         */
        // TODO(b/255371817): update documentation to reflect how this data will be used
        public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes,
                @BytesLong long uploadBytes) {
            mNetworkDownloadBytes = downloadBytes;
            mNetworkUploadBytes = uploadBytes;
            return this;
        }

        /**
         * Set the minimum size of non-resumable network traffic this job requires, in bytes. When
         * the upload or download can be easily paused and resumed, use this to set the smallest
         * size that must be transmitted between start and stop events to be considered successful.
         * If the transfer cannot be paused and resumed, then this should be the sum of the values
         * provided to {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)}.
         *
         * <p>
         * Apps are encouraged to provide values that are as accurate as possible since JobScheduler
         * will try to run the job at a time when at least the minimum chunk can be transmitted to
         * reduce the amount of repetitive data that's transferred. Jobs that cannot provide
         * reasonable estimates should use the sentinel value {@link JobInfo#NETWORK_BYTES_UNKNOWN}.
         *
         * <p>
         * The values provided here only reflect the minimum non-resumable traffic that will be
         * performed by the base job; if you're using {@link JobWorkItem} then
         * you also need to define the network traffic used by each work item
         * when constructing them.
         *
         * @param chunkSizeBytes The smallest piece of data that cannot be easily paused and
         *                       resumed, in bytes.
         * @see JobInfo#getMinimumNetworkChunkBytes()
         * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long, long)
         */
        @NonNull
        public Builder setMinimumNetworkChunkBytes(@BytesLong long chunkSizeBytes) {
            if (chunkSizeBytes != NETWORK_BYTES_UNKNOWN && chunkSizeBytes <= 0) {
                throw new IllegalArgumentException("Minimum chunk size must be positive");
            }
            mMinimumNetworkChunkBytes = chunkSizeBytes;
            return this;
        }

        /**
         * Specify that to run this job, the device must be charging (or be a
         * non-battery-powered device connected to permanent power, such as Android TV
         * devices). This defaults to {@code false}. Setting this to {@code false} <b>DOES NOT</b>
         * mean the job will only run when the device is not charging.
         *
         * <p class="note">For purposes of running jobs, a battery-powered device
         * "charging" is not quite the same as simply being connected to power.  If the
         * device is so busy that the battery is draining despite a power connection, jobs
         * with this constraint will <em>not</em> run.  This can happen during some
         * common use cases such as video chat, particularly if the device is plugged in
         * to USB rather than to wall power.
         *
         * @param requiresCharging Pass {@code true} to require that the device be
         *     charging in order to run the job.
         * @see JobInfo#isRequireCharging()
         */
        public Builder setRequiresCharging(boolean requiresCharging) {
            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_CHARGING)
                    | (requiresCharging ? CONSTRAINT_FLAG_CHARGING : 0);
            return this;
        }

        /**
         * Specify that to run this job, the device's battery level must not be low.
         * This defaults to false.  If true, the job will only run when the battery level
         * is not low, which is generally the point where the user is given a "low battery"
         * warning. Setting this to {@code false} <b>DOES NOT</b> mean the job will only run
         * when the battery is low.
         *
         * @param batteryNotLow Whether or not the device's battery level must not be low.
         * @see JobInfo#isRequireBatteryNotLow()
         */
        public Builder setRequiresBatteryNotLow(boolean batteryNotLow) {
            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_BATTERY_NOT_LOW)
                    | (batteryNotLow ? CONSTRAINT_FLAG_BATTERY_NOT_LOW : 0);
            return this;
        }

        /**
         * When set {@code true}, ensure that this job will not run if the device is in active use.
         * The default state is {@code false}: that is, the for the job to be runnable even when
         * someone is interacting with the device. Setting this to {@code false} <b>DOES NOT</b>
         * mean the job will only run when the device is not idle.
         *
         * <p>This state is a loose definition provided by the system. In general, it means that
         * the device is not currently being used interactively, 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.</p>
         *
         * <p class="note">Despite the similar naming, this job constraint is <em>not</em>
         * related to the system's "device idle" or "doze" states.  This constraint only
         * determines whether a job is allowed to run while the device is directly in use.
         *
         * @param requiresDeviceIdle Pass {@code true} to prevent the job from running
         *     while the device is being used interactively.
         * @see JobInfo#isRequireDeviceIdle()
         */
        public Builder setRequiresDeviceIdle(boolean requiresDeviceIdle) {
            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_DEVICE_IDLE)
                    | (requiresDeviceIdle ? CONSTRAINT_FLAG_DEVICE_IDLE : 0);
            return this;
        }

        /**
         * Specify that to run this job, the device's available storage must not be low.
         * This defaults to false.  If true, the job will only run when the device is not
         * in a low storage state, which is generally the point where the user is given a
         * "low storage" warning.
         * @param storageNotLow Whether or not the device's available storage must not be low.
         * @see JobInfo#isRequireStorageNotLow()
         */
        public Builder setRequiresStorageNotLow(boolean storageNotLow) {
            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_STORAGE_NOT_LOW)
                    | (storageNotLow ? CONSTRAINT_FLAG_STORAGE_NOT_LOW : 0);
            return this;
        }

        /**
         * 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 using the same job ID and
         * observing the same URIs in place of calling
         * {@link JobService#jobFinished(JobParameters, boolean)}. Remember that
         * {@link JobScheduler#schedule(JobInfo)} stops a running job if it uses the same job ID,
         * so only call it after you've finished processing the most recent changes (in other words,
         * call {@link JobScheduler#schedule(JobInfo)} where you would have normally called
         * {@link JobService#jobFinished(JobParameters, boolean)}.
         * Following this pattern will ensure you do not lose any content changes: while your
         * job is running, the system will continue monitoring for content changes, and propagate
         * any changes it sees over to the next job you schedule, so you do not have to worry
         * about missing new changes. <b>Scheduling the new job
         * before or during processing will cause the current job to be stopped (as described in
         * {@link JobScheduler#schedule(JobInfo)}), meaning the wakelock will be released for the
         * current job and your app process may be killed since it will no longer be in a valid
         * component lifecycle.</b>
         * Since {@link JobScheduler#schedule(JobInfo)} stops the current job, you do not
         * need to call {@link JobService#jobFinished(JobParameters, boolean)} if you call
         * {@link JobScheduler#schedule(JobInfo)} using the same job ID as the
         * currently running job.</p>
         *
         * <p>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.</p>
         *
         * <p>The following example shows how this feature can be used to monitor for changes
         * in the photos on a device.</p>
         *
         * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/PhotosContentJob.java
         *      job}
         *
         * @param uri The content: URI to monitor.
         * @see JobInfo#getTriggerContentUris()
         */
        public Builder addTriggerContentUri(@NonNull TriggerContentUri uri) {
            if (mTriggerContentUris == null) {
                mTriggerContentUris = new ArrayList<>();
            }
            mTriggerContentUris.add(uri);
            return this;
        }

        /**
         * Set the delay (in milliseconds) from when a content change is detected until
         * the job is scheduled.  If there are more changes during that time, the delay
         * will be reset to start at the time of the most recent change.
         * @param durationMs Delay after most recent content change, in milliseconds.
         * @see JobInfo#getTriggerContentUpdateDelay()
         */
        public Builder setTriggerContentUpdateDelay(long durationMs) {
            mTriggerContentUpdateDelay = durationMs;
            return this;
        }

        /**
         * Set the maximum total delay (in milliseconds) that is allowed from the first
         * time a content change is detected until the job is scheduled.
         * @param durationMs Delay after initial content change, in milliseconds.
         * @see JobInfo#getTriggerContentMaxDelay()
         */
        public Builder setTriggerContentMaxDelay(long durationMs) {
            mTriggerContentMaxDelay = durationMs;
            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, as long
         * as the constraints are satisfied. If the constraints are not satisfied within this
         * interval, the job will wait until the constraints are satisfied.
         * Setting this function on the builder with {@link #setMinimumLatency(long)} or
         * {@link #setOverrideDeadline(long)} will result in an error.
         * @param intervalMillis Millisecond interval for which this job will repeat.
         * @see JobInfo#getIntervalMillis()
         * @see JobInfo#getFlexMillis()
         */
        public Builder setPeriodic(long intervalMillis) {
            return setPeriodic(intervalMillis, intervalMillis);
        }

        /**
         * Specify that this job should recur with the provided interval and flex. The job can
         * execute at any time in a window of flex length at the end of the period.
         * If the constraints are not satisfied within the window,
         * the job will wait until the constraints are satisfied.
         * @param intervalMillis Millisecond interval for which this job will repeat. A minimum
         *                       value of {@link #getMinPeriodMillis()} is enforced.
         * @param flexMillis Millisecond flex for this job. Flex is clamped to be at least
         *                   {@link #getMinFlexMillis()} or 5 percent of the period, whichever is
         *                   higher.
         * @see JobInfo#getIntervalMillis()
         * @see JobInfo#getFlexMillis()
         */
        public Builder setPeriodic(long intervalMillis, long flexMillis) {
            final long minPeriod = getMinPeriodMillis();
            if (intervalMillis < minPeriod) {
                Log.w(TAG, "Requested interval " + formatDuration(intervalMillis) + " for job "
                        + mJobId + " is too small; raising to " + formatDuration(minPeriod));
                intervalMillis = minPeriod;
            }

            final long percentClamp = 5 * intervalMillis / 100;
            final long minFlex = Math.max(percentClamp, getMinFlexMillis());
            if (flexMillis < minFlex) {
                Log.w(TAG, "Requested flex " + formatDuration(flexMillis) + " for job " + mJobId
                        + " is too small; raising to " + formatDuration(minFlex));
                flexMillis = minFlex;
            }

            mIsPeriodic = true;
            mIntervalMillis = intervalMillis;
            mFlexMillis = flexMillis;
            mHasEarlyConstraint = mHasLateConstraint = true;
            return this;
        }

        /**
         * Specify that this job should be delayed by the provided amount of time. The job may not
         * run the instant the delay has elapsed. JobScheduler will start the job at an
         * indeterminate time after the delay has elapsed.
         * <p>
         * Because it doesn't make sense setting this property on a periodic job, doing so will
         * throw an {@link java.lang.IllegalArgumentException} when
         * {@link android.app.job.JobInfo.Builder#build()} is called.
         *
         * Negative latencies also don't make sense for a job and are indicative of an error,
         * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
         * setting a negative deadline will result in
         * {@link android.app.job.JobInfo.Builder#build()} throwing an
         * {@link java.lang.IllegalArgumentException}.
         *
         * @param minLatencyMillis Milliseconds before which this job will not be considered for
         *                         execution.
         * @see JobInfo#getMinLatencyMillis()
         */
        public Builder setMinimumLatency(long minLatencyMillis) {
            mMinLatencyMillis = minLatencyMillis;
            mHasEarlyConstraint = true;
            return this;
        }

        /**
         * Set a deadline after which all other functional requested constraints will be ignored.
         * After the deadline has passed, the job can run even if other requirements (including
         * a delay set through {@link #setMinimumLatency(long)}) are not met.
         * {@link JobParameters#isOverrideDeadlineExpired()} will return {@code true} if the job's
         * deadline has passed. The job's execution may be delayed beyond the set deadline by
         * other factors such as Doze mode and system health signals.
         *
         * <p>
         * Because it doesn't make sense setting this property on a periodic job, doing so will
         * throw an {@link java.lang.IllegalArgumentException} when
         * {@link android.app.job.JobInfo.Builder#build()} is called.
         *
         * <p>
         * Negative deadlines also don't make sense for a job and are indicative of an error,
         * so starting in Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
         * setting a negative deadline will result in
         * {@link android.app.job.JobInfo.Builder#build()} throwing an
         * {@link java.lang.IllegalArgumentException}.
         *
         * <p class="note">
         * Since a job will run once the deadline has passed regardless of the status of other
         * constraints, setting a deadline of 0 (or a {@link #setMinimumLatency(long) delay} equal
         * to the deadline) with other constraints makes those constraints
         * meaningless when it comes to execution decisions. Since doing so is indicative of an
         * error in the logic, starting in Android version
         * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, jobs with extremely short
         * time windows will fail to build. Time windows are
         * defined as the time between a job's {@link #setMinimumLatency(long) minimum latency}
         * and its deadline. If the minimum latency is not set, it is assumed to be 0.
         *
         * Work that must happen immediately should use {@link #setExpedited(boolean)} or
         * {@link #setUserInitiated(boolean)} in the appropriate manner.
         *
         * <p>
         * This API aimed to guarantee execution of the job by the deadline only on Android version
         * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. That aim and guarantee has not existed
         * since {@link android.os.Build.VERSION_CODES#M}.
         *
         * @see JobInfo#getMaxExecutionDelayMillis()
         */
        public Builder setOverrideDeadline(long maxExecutionDelayMillis) {
            mMaxExecutionDelayMillis = maxExecutionDelayMillis;
            mHasLateConstraint = true;
            return this;
        }

        /**
         * Set up the back-off/retry policy.
         * This defaults to some respectable values: {30 seconds, Exponential}. We cap back-off at
         * 5hrs.
         * <p>
         * Note that trying to set a backoff criteria for a job with
         * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build().
         * This is because back-off typically does not make sense for these types of jobs. See
         * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
         * for more description of the return value for the case of a job executing while in idle
         * mode.
         * @param initialBackoffMillis Millisecond time interval to wait initially when job has
         *                             failed.
         * @see JobInfo#getInitialBackoffMillis()
         * @see JobInfo#getBackoffPolicy()
         */
        public Builder setBackoffCriteria(long initialBackoffMillis,
                @BackoffPolicy int backoffPolicy) {
            final long minBackoff = getMinBackoffMillis();
            if (initialBackoffMillis < minBackoff) {
                Log.w(TAG, "Requested backoff " + formatDuration(initialBackoffMillis) + " for job "
                        + mJobId + " is too small; raising to " + formatDuration(minBackoff));
                initialBackoffMillis = minBackoff;
            }

            mBackoffPolicySet = true;
            mInitialBackoffMillis = initialBackoffMillis;
            mBackoffPolicy = backoffPolicy;
            return this;
        }

        /**
         * Setting this to true indicates that this job is important and needs to run as soon as
         * possible with stronger guarantees than regular jobs. These "expedited" jobs will:
         * <ol>
         *     <li>Run as soon as possible</li>
         *     <li>Be less restricted during Doze and battery saver</li>
         *     <li>
         *         Bypass Doze, app standby, and battery saver network restrictions (if the job
         *         has a {@link #setRequiredNetwork(NetworkRequest) connectivity constraint})
         *     </li>
         *     <li>Be less likely to be killed than regular jobs</li>
         *     <li>Be subject to background location throttling</li>
         *     <li>Be exempt from delay to optimize job execution</li>
         * </ol>
         *
         * <p>
         * Expedited jobs are given {@link #PRIORITY_MAX} by default.
         *
         * <p>
         * Since these jobs have stronger guarantees than regular jobs, they will be subject to
         * stricter quotas. As long as an app has available expedited quota, jobs scheduled with
         * this set to true will run with these guarantees. If an app has run out of available
         * expedited quota, any pending expedited jobs will run as regular jobs.
         * {@link JobParameters#isExpeditedJob()} can be used to know whether the executing job
         * has expedited guarantees or not. In addition, {@link JobScheduler#schedule(JobInfo)}
         * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have
         * available quota (and the job will not be successfully scheduled).
         *
         * <p>
         * Expedited job quota will replenish over time and as the user interacts with the app,
         * so you should not have to worry about running out of quota because of processing from
         * frequent user engagement.
         *
         * <p>
         * Expedited jobs may only set network, storage-not-low, and persistence constraints.
         * No other constraints are allowed.
         *
         * <p>
         * Assuming all constraints remain satisfied (including ideal system load conditions),
         * expedited jobs can have an execution time of at least 1 minute. If your
         * app has remaining expedited job quota, then the expedited job <i>may</i> potentially run
         * longer until remaining quota is used up. Just like with regular jobs, quota is not
         * consumed while the app is on top and visible to the user.
         *
         * <p class="note">
         * Note: Even though expedited jobs are meant to run as soon as possible, they may be
         * deferred if the system is under heavy load or requested constraints are not satisfied.
         * This delay may be true for expedited jobs of the foreground app on Android version
         * {@link Build.VERSION_CODES#S}, but starting from Android version
         * {@link Build.VERSION_CODES#TIRAMISU}, expedited jobs for the foreground app are
         * guaranteed to be started before {@link JobScheduler#schedule(JobInfo)} returns (assuming
         * all requested constraints are satisfied), similar to foreground services.
         *
         * @see JobInfo#isExpedited()
         */
        @NonNull
        public Builder setExpedited(boolean expedited) {
            if (expedited) {
                mFlags |= FLAG_EXPEDITED;
                if (mPriority == PRIORITY_DEFAULT) {
                    // The default priority for EJs is MAX, but only change this if .setPriority()
                    // hasn't been called yet.
                    mPriority = PRIORITY_MAX;
                }
            } else {
                if (mPriority == PRIORITY_MAX && (mFlags & FLAG_EXPEDITED) != 0) {
                    // Reset the priority for the job, but only change this if .setPriority()
                    // hasn't been called yet.
                    mPriority = PRIORITY_DEFAULT;
                }
                mFlags &= (~FLAG_EXPEDITED);
            }
            return this;
        }

        /**
         * Indicates that this job is being scheduled to fulfill an explicit user request.
         * As such, user-initiated jobs can only be scheduled when the app is in the foreground
         * or in a state where launching an activity is allowed, as defined
         * <a href=
         * "https://developer.android.com/guide/components/activities/background-starts#exceptions">
         * here</a>. Attempting to schedule one outside of these conditions will return a
         * {@link JobScheduler#RESULT_FAILURE}.
         *
         * <p>
         * This should <b>NOT</b> be used for automatic features.
         *
         * <p>
         * All user-initiated jobs must have an associated notification, set via
         * {@link JobService#setNotification(JobParameters, int, Notification, int)}, and will be
         * shown in the Task Manager when running. These jobs cannot be rescheduled by the app
         * if the user stops the job via system provided affordance (such as the Task Manager).
         * Thus, it is best practice and recommended to provide action buttons in the
         * associated notification to allow the user to stop the job gracefully
         * and allow for rescheduling.
         *
         * <p>
         * If the app doesn't hold the {@link android.Manifest.permission#RUN_USER_INITIATED_JOBS}
         * permission when scheduling a user-initiated job, JobScheduler will throw a
         * {@link SecurityException}.
         *
         * <p>
         * In {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, user-initiated jobs can only
         * be used for network data transfers. As such, they must specify a required network via
         * {@link #setRequiredNetwork(NetworkRequest)} or {@link #setRequiredNetworkType(int)}.
         *
         * <p>
         * These jobs will not be subject to quotas and will be started immediately once scheduled
         * if all constraints are met and the device system health allows for additional tasks.
         * They are also given {@link #PRIORITY_MAX} by default, and the priority cannot be changed.
         *
         * @see JobInfo#isUserInitiated()
         */
        @RequiresPermission(android.Manifest.permission.RUN_USER_INITIATED_JOBS)
        @NonNull
        public Builder setUserInitiated(boolean userInitiated) {
            if (userInitiated) {
                mFlags |= FLAG_USER_INITIATED;
                if (mPriority == PRIORITY_DEFAULT) {
                    // The default priority for UIJs is MAX, but only change this if .setPriority()
                    // hasn't been called yet.
                    mPriority = PRIORITY_MAX;
                }
            } else {
                if (mPriority == PRIORITY_MAX && (mFlags & FLAG_USER_INITIATED) != 0) {
                    // Reset the priority for the job, but only change this if .setPriority()
                    // hasn't been called yet.
                    mPriority = PRIORITY_DEFAULT;
                }
                mFlags &= (~FLAG_USER_INITIATED);
            }
            return this;
        }

        /**
         * Setting this to true indicates that this job is important while the scheduling app
         * is in the foreground or on the temporary allowlist for background restrictions.
         * This means that the system will relax doze restrictions on this job during this time.
         *
         * Apps should use this flag only for short jobs that are essential for the app to function
         * properly in the foreground.
         *
         * Note that once the scheduling app is no longer allowlisted from background restrictions
         * and in the background, or the job failed due to unsatisfied constraints,
         * this job should be expected to behave like other jobs without this flag.
         *
         * <p>
         * Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default.
         *
         * @param importantWhileForeground whether to relax doze restrictions for this job when the
         *                                 app is in the foreground. False by default.
         * @see JobInfo#isImportantWhileForeground()
         * @deprecated Use {@link #setExpedited(boolean)} instead.
         */
        @Deprecated
        public Builder setImportantWhileForeground(boolean importantWhileForeground) {
            if (importantWhileForeground) {
                mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
                if (mPriority == PRIORITY_DEFAULT) {
                    // The default priority for important-while-foreground is HIGH, but only change
                    // this if .setPriority() hasn't been called yet.
                    mPriority = PRIORITY_HIGH;
                }
            } else {
                if (mPriority == PRIORITY_HIGH
                        && (mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
                    // Reset the priority for the job, but only change this if .setPriority()
                    // hasn't been called yet.
                    mPriority = PRIORITY_DEFAULT;
                }
                mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND);
            }
            return this;
        }

        /**
         * Setting this to true indicates that this job is designed to prefetch
         * content that will make a material improvement to the experience of
         * the specific user of this device. For example, fetching top headlines
         * of interest to the current user.
         * <p>
         * Apps targeting Android version {@link Build.VERSION_CODES#TIRAMISU} or later are
         * not allowed to have deadlines (set via {@link #setOverrideDeadline(long)} on their
         * prefetch jobs.
         * <p>
         * The system may use this signal to relax the network constraints you
         * originally requested, such as allowing a
         * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered
         * network when there is a surplus of metered data available. The system
         * may also use this signal in combination with end user usage patterns
         * to ensure data is prefetched before the user launches your app.
         * @see JobInfo#isPrefetch()
         */
        public Builder setPrefetch(boolean prefetch) {
            if (prefetch) {
                mFlags |= FLAG_PREFETCH;
            } else {
                mFlags &= (~FLAG_PREFETCH);
            }
            return this;
        }

        /**
         * Set whether or not to persist this job across device reboots.
         *
         * @param isPersisted True to indicate that the job will be written to
         *            disk and loaded at boot.
         * @see JobInfo#isPersisted()
         */
        @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED)
        public Builder setPersisted(boolean isPersisted) {
            mIsPersisted = isPersisted;
            return this;
        }

        /**
         * Set a tag that will be used in {@link android.os.Trace traces}.
         * Since this is a trace tag, it must follow the rules set in
         * {@link android.os.Trace#beginSection(String)}, such as it cannot be more
         * than 127 Unicode code units.
         * Additionally, since leading and trailing whitespace can lead to hard-to-debug issues,
         * they will be {@link String#trim() trimmed}.
         * An empty String (after trimming) is not allowed.
         * @param traceTag The tag to use in traces.
         * @return This object for method chaining
         */
        @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
        @NonNull
        public Builder setTraceTag(@Nullable String traceTag) {
            mTraceTag = validateTraceTag(traceTag);
            return this;
        }

        /**
         * @return The job object to hand to the JobScheduler. This object is immutable.
         */
        public JobInfo build() {
            return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS),
                    Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES),
                    Compatibility.isChangeEnabled(ENFORCE_MINIMUM_TIME_WINDOWS),
                    Compatibility.isChangeEnabled(REJECT_NEGATIVE_DELAYS_AND_DEADLINES));
        }

        /** @hide */
        public JobInfo build(boolean disallowPrefetchDeadlines,
                boolean rejectNegativeNetworkEstimates,
                boolean enforceMinimumTimeWindows,
                boolean rejectNegativeDelaysAndDeadlines) {
            // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy
            // check that would ideally be phased out instead.
            if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
                throw new IllegalArgumentException("An idle mode job will not respect any" +
                        " back-off policy, so calling setBackoffCriteria with" +
                        " setRequiresDeviceIdle is an error.");
            }
            JobInfo jobInfo = new JobInfo(this);
            jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates,
                    enforceMinimumTimeWindows, rejectNegativeDelaysAndDeadlines);
            return jobInfo;
        }

        /**
         * @hide
         */
        public String summarize() {
            final String service = (mJobService != null)
                    ? mJobService.flattenToShortString()
                    : "null";
            return "JobInfo.Builder{job:" + mJobId + "/" + service + "}";
        }
    }

    /**
     * @hide
     */
    public final void enforceValidity(boolean disallowPrefetchDeadlines,
            boolean rejectNegativeNetworkEstimates,
            boolean enforceMinimumTimeWindows,
            boolean rejectNegativeDelaysAndDeadlines) {
        // Check that network estimates require network type and are reasonable values.
        if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0)
                && networkRequest == null) {
            throw new IllegalArgumentException(
                    "Can't provide estimated network usage without requiring a network");
        }
        if (networkRequest != null && rejectNegativeNetworkEstimates) {
            if (networkUploadBytes != NETWORK_BYTES_UNKNOWN && networkUploadBytes < 0) {
                throw new IllegalArgumentException(
                        "Invalid network upload bytes: " + networkUploadBytes);
            }
            if (networkDownloadBytes != NETWORK_BYTES_UNKNOWN && networkDownloadBytes < 0) {
                throw new IllegalArgumentException(
                        "Invalid network download bytes: " + networkDownloadBytes);
            }
        }
        final long estimatedTransfer;
        if (networkUploadBytes == NETWORK_BYTES_UNKNOWN) {
            estimatedTransfer = networkDownloadBytes;
        } else {
            estimatedTransfer = networkUploadBytes
                    + (networkDownloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : networkDownloadBytes);
        }
        if (minimumNetworkChunkBytes != NETWORK_BYTES_UNKNOWN
                && estimatedTransfer != NETWORK_BYTES_UNKNOWN
                && minimumNetworkChunkBytes > estimatedTransfer) {
            throw new IllegalArgumentException(
                    "Minimum chunk size can't be greater than estimated network usage");
        }
        if (minimumNetworkChunkBytes != NETWORK_BYTES_UNKNOWN && minimumNetworkChunkBytes <= 0) {
            throw new IllegalArgumentException("Minimum chunk size must be positive");
        }

        if (rejectNegativeDelaysAndDeadlines) {
            if (minLatencyMillis < 0) {
                throw new IllegalArgumentException(
                        "Minimum latency is negative: " + minLatencyMillis);
            }
            if (maxExecutionDelayMillis < 0) {
                throw new IllegalArgumentException(
                        "Override deadline is negative: " + maxExecutionDelayMillis);
            }
        }

        final boolean hasDeadline = maxExecutionDelayMillis != 0L;
        // Check that a deadline was not set on a periodic job.
        if (isPeriodic) {
            if (hasDeadline) {
                throw new IllegalArgumentException(
                        "Can't call setOverrideDeadline() on a periodic job.");
            }
            if (minLatencyMillis != 0L) {
                throw new IllegalArgumentException(
                        "Can't call setMinimumLatency() on a periodic job");
            }
            if (triggerContentUris != null) {
                throw new IllegalArgumentException(
                        "Can't call addTriggerContentUri() on a periodic job");
            }
        }

        // Prefetch jobs should not have deadlines
        if (disallowPrefetchDeadlines && hasDeadline && (flags & FLAG_PREFETCH) != 0) {
            throw new IllegalArgumentException(
                    "Can't call setOverrideDeadline() on a prefetch job.");
        }

        if (isPersisted) {
            // We can't serialize network specifiers
            if (networkRequest != null
                    && networkRequest.getNetworkSpecifier() != null) {
                throw new IllegalArgumentException(
                        "Network specifiers aren't supported for persistent jobs");
            }
            if (triggerContentUris != null) {
                throw new IllegalArgumentException(
                        "Can't call addTriggerContentUri() on a persisted job");
            }
            if (!transientExtras.isEmpty()) {
                throw new IllegalArgumentException(
                        "Can't call setTransientExtras() on a persisted job");
            }
            if (clipData != null) {
                throw new IllegalArgumentException(
                        "Can't call setClipData() on a persisted job");
            }
        }

        if ((flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
            if (hasEarlyConstraint) {
                throw new IllegalArgumentException(
                        "An important while foreground job cannot have a time delay");
            }
            if (mPriority != PRIORITY_HIGH && mPriority != PRIORITY_DEFAULT) {
                throw new IllegalArgumentException(
                        "An important while foreground job must be high or default priority."
                                + " Don't mark unimportant tasks as important while foreground.");
            }
        }

        final boolean isExpedited = (flags & FLAG_EXPEDITED) != 0;
        final boolean isUserInitiated = (flags & FLAG_USER_INITIATED) != 0;
        switch (mPriority) {
            case PRIORITY_MAX:
                if (!(isExpedited || isUserInitiated)) {
                    throw new IllegalArgumentException(
                            "Only expedited or user-initiated jobs can have max priority");
                }
                break;
            case PRIORITY_HIGH:
                if ((flags & FLAG_PREFETCH) != 0) {
                    throw new IllegalArgumentException("Prefetch jobs cannot be high priority");
                }
                if (isPeriodic) {
                    throw new IllegalArgumentException("Periodic jobs cannot be high priority");
                }
                break;
            case PRIORITY_DEFAULT:
            case PRIORITY_LOW:
            case PRIORITY_MIN:
                break;
            default:
                throw new IllegalArgumentException("Invalid priority level provided: " + mPriority);
        }

        final boolean hasFunctionalConstraint = networkRequest != null
                || constraintFlags != 0
                || (triggerContentUris != null && triggerContentUris.length > 0);
        if (hasLateConstraint && !isPeriodic) {
            if (!hasFunctionalConstraint) {
                Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'"
                        + " has a deadline with no functional constraints."
                        + " The deadline won't improve job execution latency."
                        + " Consider removing the deadline.");
            } else {
                final long windowStart = hasEarlyConstraint ? minLatencyMillis : 0;
                if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) {
                    if (enforceMinimumTimeWindows
                            && Flags.enforceMinimumTimeWindows()) {
                        throw new IllegalArgumentException("Time window too short. Constraints"
                                + " unlikely to be satisfied. Increase deadline to a reasonable"
                                + " duration."
                                + " Job '" + service.flattenToShortString() + "#" + jobId + "'"
                                + " has delay=" + windowStart
                                + ", deadline=" + maxExecutionDelayMillis);
                    } else {
                        Log.w(TAG, "Job '" + service.flattenToShortString() + "#" + jobId + "'"
                                + " has a deadline with functional constraints and an extremely"
                                + " short time window of "
                                + (maxExecutionDelayMillis - windowStart) + " ms"
                                + " (delay=" + windowStart
                                + ", deadline=" + maxExecutionDelayMillis + ")."
                                + " The functional constraints are not likely to be satisfied when"
                                + " the job runs.");
                    }
                }
            }
        }

        if (isExpedited) {
            if (hasEarlyConstraint) {
                throw new IllegalArgumentException("An expedited job cannot have a time delay");
            }
            if (hasLateConstraint) {
                throw new IllegalArgumentException("An expedited job cannot have a deadline");
            }
            if (isPeriodic) {
                throw new IllegalArgumentException("An expedited job cannot be periodic");
            }
            if (isUserInitiated) {
                throw new IllegalArgumentException("An expedited job cannot be user-initiated");
            }
            if (mPriority != PRIORITY_MAX && mPriority != PRIORITY_HIGH) {
                throw new IllegalArgumentException(
                        "An expedited job must be high or max priority. Don't use expedited jobs"
                                + " for unimportant tasks.");
            }
            if ((constraintFlags & ~CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0
                    || (flags & ~(FLAG_EXPEDITED | FLAG_EXEMPT_FROM_APP_STANDBY)) != 0) {
                throw new IllegalArgumentException(
                        "An expedited job can only have network and storage-not-low constraints");
            }
            if (triggerContentUris != null && triggerContentUris.length > 0) {
                throw new IllegalArgumentException(
                        "Can't call addTriggerContentUri() on an expedited job");
            }
        }

        if (isUserInitiated) {
            if (hasEarlyConstraint) {
                throw new IllegalArgumentException("A user-initiated job cannot have a time delay");
            }
            if (hasLateConstraint) {
                throw new IllegalArgumentException("A user-initiated job cannot have a deadline");
            }
            if (isPeriodic) {
                throw new IllegalArgumentException("A user-initiated job cannot be periodic");
            }
            if ((flags & FLAG_PREFETCH) != 0) {
                throw new IllegalArgumentException(
                        "A user-initiated job cannot also be a prefetch job");
            }
            if (mPriority != PRIORITY_MAX) {
                throw new IllegalArgumentException("A user-initiated job must be max priority.");
            }
            if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
                throw new IllegalArgumentException(
                        "A user-initiated job cannot have a device-idle constraint");
            }
            if (triggerContentUris != null && triggerContentUris.length > 0) {
                throw new IllegalArgumentException(
                        "Can't call addTriggerContentUri() on a user-initiated job");
            }
            // UIDTs
            if (networkRequest == null) {
                throw new IllegalArgumentException(
                        "A user-initiated data transfer job must specify a valid network type");
            }
        }

        if (mDebugTags.size() > MAX_NUM_DEBUG_TAGS) {
            throw new IllegalArgumentException(
                    "Can't have more than " + MAX_NUM_DEBUG_TAGS + " tags");
        }
        final ArraySet<String> validatedDebugTags = new ArraySet<>();
        for (int i = 0; i < mDebugTags.size(); ++i) {
            validatedDebugTags.add(validateDebugTag(mDebugTags.valueAt(i)));
        }
        mDebugTags.clear();
        mDebugTags.addAll(validatedDebugTags);

        validateTraceTag(mTraceTag);
    }

    /**
     * Returns a sanitized debug tag if valid, or throws an exception if not.
     * @hide
     */
    @NonNull
    public static String validateDebugTag(@Nullable String debugTag) {
        if (debugTag == null) {
            throw new NullPointerException("debug tag cannot be null");
        }
        debugTag = debugTag.trim();
        if (debugTag.isEmpty()) {
            throw new IllegalArgumentException("debug tag cannot be empty");
        }
        if (debugTag.length() > MAX_DEBUG_TAG_LENGTH) {
            throw new IllegalArgumentException(
                    "debug tag cannot be more than " + MAX_DEBUG_TAG_LENGTH + " characters");
        }
        return debugTag.intern();
    }

    /**
     * Returns a sanitized trace tag if valid, or throws an exception if not.
     * @hide
     */
    @Nullable
    public static String validateTraceTag(@Nullable String traceTag) {
        if (traceTag == null) {
            return null;
        }
        traceTag = traceTag.trim();
        if (traceTag.isEmpty()) {
            throw new IllegalArgumentException("trace tag cannot be empty");
        }
        if (traceTag.length() > MAX_TRACE_TAG_LENGTH) {
            throw new IllegalArgumentException(
                    "traceTag tag cannot be more than " + MAX_TRACE_TAG_LENGTH + " characters");
        }
        if (traceTag.contains("|") || traceTag.contains("\n") || traceTag.contains("\0")) {
            throw new IllegalArgumentException("Trace tag cannot contain |, \\n, or \\0");
        }
        return traceTag.intern();
    }

    /**
     * Convert a bias integer into a human readable string for debugging.
     * @hide
     */
    public static String getBiasString(int bias) {
        switch (bias) {
            case BIAS_DEFAULT:
                return BIAS_DEFAULT + " [DEFAULT]";
            case BIAS_SYNC_EXPEDITED:
                return BIAS_SYNC_EXPEDITED + " [SYNC_EXPEDITED]";
            case BIAS_SYNC_INITIALIZATION:
                return BIAS_SYNC_INITIALIZATION + " [SYNC_INITIALIZATION]";
            case BIAS_BOUND_FOREGROUND_SERVICE:
                return BIAS_BOUND_FOREGROUND_SERVICE + " [BFGS_APP]";
            case BIAS_FOREGROUND_SERVICE:
                return BIAS_FOREGROUND_SERVICE + " [FGS_APP]";
            case BIAS_TOP_APP:
                return BIAS_TOP_APP + " [TOP_APP]";

                // BIAS_ADJ_* are adjustments and not used as real priorities.
                // No need to convert to strings.
        }
        return bias + " [UNKNOWN]";
    }

    /**
     * Convert a priority integer into a human readable string for debugging.
     * @hide
     */
    public static String getPriorityString(@Priority int priority) {
        switch (priority) {
            case PRIORITY_MIN:
                return priority + " [MIN]";
            case PRIORITY_LOW:
                return priority + " [LOW]";
            case PRIORITY_DEFAULT:
                return priority + " [DEFAULT]";
            case PRIORITY_HIGH:
                return priority + " [HIGH]";
            case PRIORITY_MAX:
                return priority + " [MAX]";
        }
        return priority + " [UNKNOWN]";
    }
}
