content: /*
   * 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_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.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresPermission;
  import android.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.util.Log;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Objects;
  
  /**
   * Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the
   * parameters required to schedule work against the calling application. These are constructed
   * using the {@link JobInfo.Builder}.
   * You must specify at least one sort of constraint on the JobInfo object that you are creating.
   * The goal here is to provide the scheduler with high-level semantics about the work you want to
   * accomplish. Doing otherwise with throw an exception in your app.
   */
  public class JobInfo implements Parcelable {
      private static String TAG = "JobInfo";
  
      /** @hide */
      @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
              NETWORK_TYPE_NONE,
              NETWORK_TYPE_ANY,
              NETWORK_TYPE_UNMETERED,
              NETWORK_TYPE_NOT_ROAMING,
              NETWORK_TYPE_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
  
      /**
       * Minimum backoff interval for a job, in milliseconds
       * @hide
       */
      public static final long MIN_BACKOFF_MILLIS = 10 * 1000L;      // 10 seconds
  
      /**
       * Query the minimum interval allowed for periodic scheduled jobs.  Attempting
       * to declare a smaller period that this when scheduling a job will result in a
       * job that is still periodic, but will run with this effective period.
       *
       * @return The minimum available interval for scheduling periodic jobs, in milliseconds.
       */
      public static final long getMinPeriodMillis() {
          return MIN_PERIOD_MILLIS;
      }
  
      /**
       * Query the minimum flex time allowed for periodic scheduled jobs.  Attempting
       * to declare a shorter flex time than this when scheduling such a job will
       * result in this amount as the effective flex time for the job.
       *
       * @return The minimum available flex time for scheduling periodic jobs, in milliseconds.
       */
      public static final long getMinFlexMillis() {
          return MIN_FLEX_MILLIS;
      }
  
      /**
       * Query the minimum automatic-reschedule backoff interval permitted for jobs.
       * @hide
       */
      public static final long getMinBackoffMillis() {
          return MIN_BACKOFF_MILLIS;
      }
  
      /**
       * Default type of backoff.
       * @hide
       */
      public static final int DEFAULT_BACKOFF_POLICY = BACKOFF_POLICY_EXPONENTIAL;
  
      /**
       * Default of {@link #getPriority}.
       * @hide
       */
      public static final int PRIORITY_DEFAULT = 0;
  
      /**
       * Value of {@link #getPriority} for expedited syncs.
       * @hide
       */
      public static final int PRIORITY_SYNC_EXPEDITED = 10;
  
      /**
       * Value of {@link #getPriority} for first time initialization syncs.
       * @hide
       */
      public static final int PRIORITY_SYNC_INITIALIZATION = 20;
  
      /**
       * Value of {@link #getPriority} for a BFGS app (overrides the supplied
       * JobInfo priority if it is smaller).
       * @hide
       */
      public static final int PRIORITY_BOUND_FOREGROUND_SERVICE = 30;
  
      /** @hide For backward compatibility. */
      @UnsupportedAppUsage
      public static final int PRIORITY_FOREGROUND_APP = PRIORITY_BOUND_FOREGROUND_SERVICE;
  
      /**
       * Value of {@link #getPriority} for a FG service app (overrides the supplied
       * JobInfo priority if it is smaller).
       * @hide
       */
      @UnsupportedAppUsage
      public static final int PRIORITY_FOREGROUND_SERVICE = 35;
  
      /**
       * Value of {@link #getPriority} for the current top app (overrides the supplied
       * JobInfo priority if it is smaller).
       * @hide
       */
      public static final int PRIORITY_TOP_APP = 40;
  
      /**
       * Adjustment of {@link #getPriority} if the app has often (50% or more of the time)
       * been running jobs.
       * @hide
       */
      public static final int PRIORITY_ADJ_OFTEN_RUNNING = -40;
  
      /**
       * Adjustment of {@link #getPriority} if the app has always (90% or more of the time)
       * been running jobs.
       * @hide
       */
      public static final int PRIORITY_ADJ_ALWAYS_RUNNING = -80;
  
      /**
       * Indicates that the implementation of this job will be using
       * {@link JobService#startForeground(int, android.app.Notification)} to run
       * in the foreground.
       * <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
      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 whitelist
       * @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 constrant must not have it.
       *
       * @hide
       */
      public static final int FLAG_EXEMPT_FROM_APP_STANDBY = 1 << 3;
  
      /**
       * @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;
  
      @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 minLatencyMillis;
      private final long maxExecutionDelayMillis;
      private final boolean isPeriodic;
      private final boolean isPersisted;
      private final long intervalMillis;
      private final long flexMillis;
      private final long initialBackoffMillis;
      private final int backoffPolicy;
      private final int priority;
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private final int flags;
  
      /**
       * Unique job id associated with this application (uid).  This is the same job ID
       * you supplied in the {@link Builder} constructor.
       */
      public int getId() {
          return jobId;
      }
  
      /**
       * @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 getPriority() {
          return priority;
      }
  
      /** @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.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) {
              return NETWORK_TYPE_UNMETERED;
          } else if (networkRequest.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
              return NETWORK_TYPE_NOT_ROAMING;
          } else if (networkRequest.networkCapabilities.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;
      }
  
      /**
       * @deprecated replaced by {@link #getEstimatedNetworkDownloadBytes()} and
       *             {@link #getEstimatedNetworkUploadBytes()}.
       * @removed
       */
      @Deprecated
      public @BytesLong long getEstimatedNetworkBytes() {
          if (networkDownloadBytes == NETWORK_BYTES_UNKNOWN
                  && networkUploadBytes == NETWORK_BYTES_UNKNOWN) {
              return NETWORK_BYTES_UNKNOWN;
          } else if (networkDownloadBytes == NETWORK_BYTES_UNKNOWN) {
              return networkUploadBytes;
          } else if (networkUploadBytes == NETWORK_BYTES_UNKNOWN) {
              return networkDownloadBytes;
          } else {
              return networkDownloadBytes + networkUploadBytes;
          }
      }
  
      /**
       * 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;
      }
  
      /**
       * 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 minLatencyMillis;
      }
  
      /**
       * @see JobInfo.Builder#setOverrideDeadline(long)
       */
      public long getMaxExecutionDelayMillis() {
          return 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#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;
      }
  
      private static boolean kindofEqualsBundle(BaseBundle a, BaseBundle b) {
          return (a == b) || (a != null && a.kindofEquals(b));
      }
  
      @Override
      public boolean equals(Object o) {
          if (!(o instanceof JobInfo)) {
              return false;
          }
          JobInfo j = (JobInfo) o;
          if (jobId != j.jobId) {
              return false;
          }
          // XXX won't be correct if one is parcelled and the other not.
          if (!kindofEqualsBundle(extras, j.extras)) {
              return false;
          }
          // XXX won't be correct if one is parcelled and the other not.
          if (!kindofEqualsBundle(transientExtras, j.transientExtras)) {
              return false;
          }
          // XXX for now we consider two different clip data objects to be different,
          // regardless of whether their contents are the same.
          if (clipData != j.clipData) {
              return false;
          }
          if (clipGrantFlags != j.clipGrantFlags) {
              return false;
          }
          if (!Objects.equals(service, j.service)) {
              return false;
          }
          if (constraintFlags != j.constraintFlags) {
              return false;
          }
          if (!Arrays.equals(triggerContentUris, j.triggerContentUris)) {
              return false;
          }
          if (triggerContentUpdateDelay != j.triggerContentUpdateDelay) {
              return false;
          }
          if (triggerContentMaxDelay != j.triggerContentMaxDelay) {
              return false;
          }
          if (hasEarlyConstraint != j.hasEarlyConstraint) {
              return false;
          }
          if (hasLateConstraint != j.hasLateConstraint) {
              return false;
          }
          if (!Objects.equals(networkRequest, j.networkRequest)) {
              return false;
          }
          if (networkDownloadBytes != j.networkDownloadBytes) {
              return false;
          }
          if (networkUploadBytes != j.networkUploadBytes) {
              return false;
          }
          if (minLatencyMillis != j.minLatencyMillis) {
              return false;
          }
          if (maxExecutionDelayMillis != j.maxExecutionDelayMillis) {
              return false;
          }
          if (isPeriodic != j.isPeriodic) {
              return false;
          }
          if (isPersisted != j.isPersisted) {
              return false;
          }
          if (intervalMillis != j.intervalMillis) {
              return false;
          }
          if (flexMillis != j.flexMillis) {
              return false;
          }
          if (initialBackoffMillis != j.initialBackoffMillis) {
              return false;
          }
          if (backoffPolicy != j.backoffPolicy) {
              return false;
          }
          if (priority != j.priority) {
              return false;
          }
          if (flags != j.flags) {
              return false;
          }
          return true;
      }
  
      @Override
      public int hashCode() {
          int hashCode = jobId;
          if (extras != null) {
              hashCode = 31 * hashCode + extras.hashCode();
          }
          if (transientExtras != null) {
              hashCode = 31 * hashCode + transientExtras.hashCode();
          }
          if (clipData != null) {
              hashCode = 31 * hashCode + clipData.hashCode();
          }
          hashCode = 31*hashCode + clipGrantFlags;
          if (service != null) {
              hashCode = 31 * hashCode + service.hashCode();
          }
          hashCode = 31 * hashCode + constraintFlags;
          if (triggerContentUris != null) {
              hashCode = 31 * hashCode + Arrays.hashCode(triggerContentUris);
          }
          hashCode = 31 * hashCode + Long.hashCode(triggerContentUpdateDelay);
          hashCode = 31 * hashCode + Long.hashCode(triggerContentMaxDelay);
          hashCode = 31 * hashCode + Boolean.hashCode(hasEarlyConstraint);
          hashCode = 31 * hashCode + Boolean.hashCode(hasLateConstraint);
          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(minLatencyMillis);
          hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis);
          hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic);
          hashCode = 31 * hashCode + Boolean.hashCode(isPersisted);
          hashCode = 31 * hashCode + Long.hashCode(intervalMillis);
          hashCode = 31 * hashCode + Long.hashCode(flexMillis);
          hashCode = 31 * hashCode + Long.hashCode(initialBackoffMillis);
          hashCode = 31 * hashCode + backoffPolicy;
          hashCode = 31 * hashCode + priority;
          hashCode = 31 * hashCode + flags;
          return hashCode;
      }
  
      private JobInfo(Parcel in) {
          jobId = in.readInt();
          extras = in.readPersistableBundle();
          transientExtras = in.readBundle();
          if (in.readInt() != 0) {
              clipData = ClipData.CREATOR.createFromParcel(in);
              clipGrantFlags = in.readInt();
          } else {
              clipData = null;
              clipGrantFlags = 0;
          }
          service = in.readParcelable(null);
          constraintFlags = in.readInt();
          triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
          triggerContentUpdateDelay = in.readLong();
          triggerContentMaxDelay = in.readLong();
          if (in.readInt() != 0) {
              networkRequest = NetworkRequest.CREATOR.createFromParcel(in);
          } else {
              networkRequest = null;
          }
          networkDownloadBytes = in.readLong();
          networkUploadBytes = 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;
          priority = in.readInt();
          flags = in.readInt();
      }
  
      private JobInfo(JobInfo.Builder b) {
          jobId = b.mJobId;
          extras = b.mExtras.deepCopy();
          transientExtras = b.mTransientExtras.deepCopy();
          clipData = b.mClipData;
          clipGrantFlags = b.mClipGrantFlags;
          service = b.mJobService;
          constraintFlags = b.mConstraintFlags;
          triggerContentUris = b.mTriggerContentUris != null
                  ? b.mTriggerContentUris.toArray(new TriggerContentUri[b.mTriggerContentUris.size()])
                  : null;
          triggerContentUpdateDelay = b.mTriggerContentUpdateDelay;
          triggerContentMaxDelay = b.mTriggerContentMaxDelay;
          networkRequest = b.mNetworkRequest;
          networkDownloadBytes = b.mNetworkDownloadBytes;
          networkUploadBytes = b.mNetworkUploadBytes;
          minLatencyMillis = b.mMinLatencyMillis;
          maxExecutionDelayMillis = b.mMaxExecutionDelayMillis;
          isPeriodic = b.mIsPeriodic;
          isPersisted = b.mIsPersisted;
          intervalMillis = b.mIntervalMillis;
          flexMillis = b.mFlexMillis;
          initialBackoffMillis = b.mInitialBackoffMillis;
          backoffPolicy = b.mBackoffPolicy;
          hasEarlyConstraint = b.mHasEarlyConstraint;
          hasLateConstraint = b.mHasLateConstraint;
          priority = b.mPriority;
          flags = b.mFlags;
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel out, int flags) {
          out.writeInt(jobId);
          out.writePersistableBundle(extras);
          out.writeBundle(transientExtras);
          if (clipData != null) {
              out.writeInt(1);
              clipData.writeToParcel(out, flags);
              out.writeInt(clipGrantFlags);
          } else {
              out.writeInt(0);
          }
          out.writeParcelable(service, flags);
          out.writeInt(constraintFlags);
          out.writeTypedArray(triggerContentUris, flags);
          out.writeLong(triggerContentUpdateDelay);
          out.writeLong(triggerContentMaxDelay);
          if (networkRequest != null) {
              out.writeInt(1);
              networkRequest.writeToParcel(out, flags);
          } else {
              out.writeInt(0);
          }
          out.writeLong(networkDownloadBytes);
          out.writeLong(networkUploadBytes);
          out.writeLong(minLatencyMillis);
          out.writeLong(maxExecutionDelayMillis);
          out.writeInt(isPeriodic ? 1 : 0);
          out.writeInt(isPersisted ? 1 : 0);
          out.writeLong(intervalMillis);
          out.writeLong(flexMillis);
          out.writeLong(initialBackoffMillis);
          out.writeInt(backoffPolicy);
          out.writeInt(hasEarlyConstraint ? 1 : 0);
          out.writeInt(hasLateConstraint ? 1 : 0);
          out.writeInt(priority);
          out.writeInt(this.flags);
      }
  
      public static final @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 = 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 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 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;
  
          /**
           * 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;
          }
  
          /** @hide */
          @UnsupportedAppUsage
          public Builder setPriority(int priority) {
              mPriority = priority;
              return this;
          }
  
          /** @hide */
          @UnsupportedAppUsage
          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.
           * <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);
  
                  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.
           *
           * @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;
          }
  
          /**
           * @deprecated replaced by
           *             {@link #setEstimatedNetworkBytes(long, long)}.
           * @removed
           */
          @Deprecated
          public Builder setEstimatedNetworkBytes(@BytesLong long networkBytes) {
              return setEstimatedNetworkBytes(networkBytes, NETWORK_BYTES_UNKNOWN);
          }
  
          /**
           * 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.
           * <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.
           *
           * @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)
           */
          public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes,
                  @BytesLong long uploadBytes) {
              mNetworkDownloadBytes = downloadBytes;
              mNetworkUploadBytes = uploadBytes;
              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}.
           *
           * <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.
           * @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.
           *
           * <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 observing the same URIs
           * before you finish execution of the JobService handling the most recent changes.
           * Following this pattern will ensure you do not lost any content changes: while your
           * job is running, the system will continue monitoring for content changes, and propagate
           * any it sees over to the next job you schedule.</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.
           * 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.
           * @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.
           * 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.
           * @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 deadline which is the maximum scheduling latency. The job will be run by this
           * deadline even if other requirements are not met. 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.
           * @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.
           * 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 while the scheduling app
           * is in the foreground or on the temporary whitelist 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 whitelisted 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.
           *
           * @param importantWhileForeground whether to relax doze restrictions for this job when the
           *                                 app is in the foreground. False by default.
           * @see JobInfo#isImportantWhileForeground()
           */
          public Builder setImportantWhileForeground(boolean importantWhileForeground) {
              if (importantWhileForeground) {
                  mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
              } else {
                  mFlags &= (~FLAG_IMPORTANT_WHILE_FOREGROUND);
              }
              return this;
          }
  
          /**
           * @removed
           * @deprecated replaced with {@link #setPrefetch(boolean)}
           */
          @Deprecated
          public Builder setIsPrefetch(boolean isPrefetch) {
              return setPrefetch(isPrefetch);
          }
  
          /**
           * 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>
           * 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;
          }
  
          /**
           * @return The job object to hand to the JobScheduler. This object is immutable.
           */
          public JobInfo build() {
              // Check that network estimates require network type
              if ((mNetworkDownloadBytes > 0 || mNetworkUploadBytes > 0) && mNetworkRequest == null) {
                  throw new IllegalArgumentException(
                          "Can't provide estimated network usage without requiring a network");
              }
              // We can't serialize network specifiers
              if (mIsPersisted && mNetworkRequest != null
                      && mNetworkRequest.networkCapabilities.getNetworkSpecifier() != null) {
                  throw new IllegalArgumentException(
                          "Network specifiers aren't supported for persistent jobs");
              }
              // Check that a deadline was not set on a periodic job.
              if (mIsPeriodic) {
                  if (mMaxExecutionDelayMillis != 0L) {
                      throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
                              "periodic job.");
                  }
                  if (mMinLatencyMillis != 0L) {
                      throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
                              "periodic job");
                  }
                  if (mTriggerContentUris != null) {
                      throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
                              "periodic job");
                  }
              }
              if (mIsPersisted) {
                  if (mTriggerContentUris != null) {
                      throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
                              "persisted job");
                  }
                  if (!mTransientExtras.isEmpty()) {
                      throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
                              "persisted job");
                  }
                  if (mClipData != null) {
                      throw new IllegalArgumentException("Can't call setClipData() on a " +
                              "persisted job");
                  }
              }
              if ((mFlags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0 && mHasEarlyConstraint) {
                  throw new IllegalArgumentException("An important while foreground job cannot "
                          + "have a time delay");
              }
              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.");
              }
              return new JobInfo(this);
          }
  
          /**
           * @hide
           */
          public String summarize() {
              final String service = (mJobService != null)
                      ? mJobService.flattenToShortString()
                      : "null";
              return "JobInfo.Builder{job:" + mJobId + "/" + service + "}";
          }
      }
  
      /**
       * Convert a priority integer into a human readable string for debugging.
       * @hide
       */
      public static String getPriorityString(int priority) {
          switch (priority) {
              case PRIORITY_DEFAULT:
                  return PRIORITY_DEFAULT + " [DEFAULT]";
              case PRIORITY_SYNC_EXPEDITED:
                  return PRIORITY_SYNC_EXPEDITED + " [SYNC_EXPEDITED]";
              case PRIORITY_SYNC_INITIALIZATION:
                  return PRIORITY_SYNC_INITIALIZATION + " [SYNC_INITIALIZATION]";
              case PRIORITY_BOUND_FOREGROUND_SERVICE:
                  return PRIORITY_BOUND_FOREGROUND_SERVICE + " [BFGS_APP]";
              case PRIORITY_FOREGROUND_SERVICE:
                  return PRIORITY_FOREGROUND_SERVICE + " [FGS_APP]";
              case PRIORITY_TOP_APP:
                  return PRIORITY_TOP_APP + " [TOP_APP]";
  
                  // PRIORITY_ADJ_* are adjustments and not used as real priorities.
                  // No need to convert to strings.
          }
          return priority + " [UNKNOWN]";
      }
  }
  