Add job persistance as a setter in the API

Bug: 15936795
Change-Id: I11e5a722bab5838dc151670256ed09dfaa7fdaa7
diff --git a/api/current.txt b/api/current.txt
index 79c4abd..12fb061 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5492,6 +5492,7 @@
     method public int getNetworkCapabilities();
     method public android.content.ComponentName getService();
     method public boolean isPeriodic();
+    method public boolean isPersisted();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
     method public void writeToParcel(android.os.Parcel, int);
@@ -5508,6 +5509,7 @@
     method public android.app.job.JobInfo build();
     method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
     method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
+    method public android.app.job.JobInfo.Builder setIsPersisted(boolean);
     method public android.app.job.JobInfo.Builder setMinimumLatency(long);
     method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
     method public android.app.job.JobInfo.Builder setPeriodic(long);
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index a22e4cd..70f6966 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -64,7 +64,6 @@
     }
 
     private final int jobId;
-    // TODO: Change this to use PersistableBundle when that lands in master.
     private final PersistableBundle extras;
     private final ComponentName service;
     private final boolean requireCharging;
@@ -75,6 +74,7 @@
     private final long minLatencyMillis;
     private final long maxExecutionDelayMillis;
     private final boolean isPeriodic;
+    private final boolean isPersisted;
     private final long intervalMillis;
     private final long initialBackoffMillis;
     private final int backoffPolicy;
@@ -145,6 +145,13 @@
     }
 
     /**
+     * @return Whether or not this job should be persisted across device reboots.
+     */
+    public boolean isPersisted() {
+        return isPersisted;
+    }
+
+    /**
      * Set to the interval between occurrences of this job. This value is <b>not</b> set if the
      * job does not recur periodically.
      */
@@ -197,6 +204,7 @@
         minLatencyMillis = in.readLong();
         maxExecutionDelayMillis = in.readLong();
         isPeriodic = in.readInt() == 1;
+        isPersisted = in.readInt() == 1;
         intervalMillis = in.readLong();
         initialBackoffMillis = in.readLong();
         backoffPolicy = in.readInt();
@@ -214,6 +222,7 @@
         minLatencyMillis = b.mMinLatencyMillis;
         maxExecutionDelayMillis = b.mMaxExecutionDelayMillis;
         isPeriodic = b.mIsPeriodic;
+        isPersisted = b.mIsPersisted;
         intervalMillis = b.mIntervalMillis;
         initialBackoffMillis = b.mInitialBackoffMillis;
         backoffPolicy = b.mBackoffPolicy;
@@ -237,6 +246,7 @@
         out.writeLong(minLatencyMillis);
         out.writeLong(maxExecutionDelayMillis);
         out.writeInt(isPeriodic ? 1 : 0);
+        out.writeInt(isPersisted ? 1 : 0);
         out.writeLong(intervalMillis);
         out.writeLong(initialBackoffMillis);
         out.writeInt(backoffPolicy);
@@ -265,6 +275,7 @@
         private boolean mRequiresCharging;
         private boolean mRequiresDeviceIdle;
         private int mNetworkCapabilities;
+        private boolean mIsPersisted;
         // One-off parameters.
         private long mMinLatencyMillis;
         private long mMaxExecutionDelayMillis;
@@ -342,11 +353,6 @@
          * 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.
-         * A periodic job will be repeated until the phone is turned off, however it will only be
-         * persisted beyond boot if the client app has declared the
-         * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED} permission. You can schedule
-         * periodic jobs without this permission, they simply will cease to exist after the phone
-         * restarts.
          * 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.
@@ -407,6 +413,19 @@
         }
 
         /**
+         * Set whether or not to persist this job across device reboots. This will only have an
+         * effect if your application holds the permission
+         * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED}. Otherwise an exception will
+         * be thrown.
+         * @param isPersisted True to indicate that the job will be written to disk and loaded at
+         *                    boot.
+         */
+        public Builder setIsPersisted(boolean isPersisted) {
+            mIsPersisted = isPersisted;
+            return this;
+        }
+
+        /**
          * @return The job object to hand to the JobScheduler. This object is immutable.
          */
         public JobInfo build() {
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index cab2728..7f8b232 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -140,12 +140,10 @@
      * This cancels the job if it's already been scheduled, and replaces it with the one provided.
      * @param job JobInfo object containing execution parameters
      * @param uId The package identifier of the application this job is for.
-     * @param canPersistJob Whether or not the client has the appropriate permissions for
-     *                       persisting this job.
      * @return Result of this operation. See <code>JobScheduler#RESULT_*</code> return codes.
      */
-    public int schedule(JobInfo job, int uId, boolean canPersistJob) {
-        JobStatus jobStatus = new JobStatus(job, uId, canPersistJob);
+    public int schedule(JobInfo job, int uId) {
+        JobStatus jobStatus = new JobStatus(job, uId);
         cancelJob(uId, job.getId());
         startTrackingJob(jobStatus);
         return JobScheduler.RESULT_SUCCESS;
@@ -668,11 +666,16 @@
             final int uid = Binder.getCallingUid();
 
             enforceValidJobRequest(uid, job);
-            final boolean canPersist = canPersistJobs(pid, uid);
+            if (job.isPersisted()) {
+                if (!canPersistJobs(pid, uid)) {
+                    throw new IllegalArgumentException("Error: requested job be persisted without"
+                            + " holding RECEIVE_BOOT_COMPLETED permission.");
+                }
+            }
 
             long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.schedule(job, uid, canPersist);
+                return JobSchedulerService.this.schedule(job, uid);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index 4ac26c1..8736980 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -160,7 +160,9 @@
             }
             return false;
         }
-        maybeWriteStatusToDiskAsync();
+        if (jobStatus.isPersisted()) {
+            maybeWriteStatusToDiskAsync();
+        }
         return removed;
     }
 
@@ -429,7 +431,8 @@
             }
         }
 
-        private List<JobStatus> readJobMapImpl(FileInputStream fis) throws XmlPullParserException, IOException {
+        private List<JobStatus> readJobMapImpl(FileInputStream fis)
+                throws XmlPullParserException, IOException {
             XmlPullParser parser = Xml.newPullParser();
             parser.setInput(fis, null);
 
@@ -498,6 +501,7 @@
             // Read out job identifier attributes.
             try {
                 jobBuilder = buildBuilderFromXml(parser);
+                jobBuilder.setIsPersisted(true);
                 uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
             } catch (NumberFormatException e) {
                 Slog.e(TAG, "Error parsing job's required fields, skipping");
diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java
index 53337c4..9ee2869 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -43,9 +43,6 @@
     final JobInfo job;
     final int uId;
 
-    /** At reschedule time we need to know whether to update job on disk. */
-    final boolean persisted;
-
     // Constraints.
     final AtomicBoolean chargingConstraintSatisfied = new AtomicBoolean();
     final AtomicBoolean timeDelayConstraintSatisfied = new AtomicBoolean();
@@ -72,16 +69,15 @@
         return uId;
     }
 
-    private JobStatus(JobInfo job, int uId, boolean persisted, int numFailures) {
+    private JobStatus(JobInfo job, int uId, int numFailures) {
         this.job = job;
         this.uId = uId;
         this.numFailures = numFailures;
-        this.persisted = persisted;
     }
 
     /** Create a newly scheduled job. */
-    public JobStatus(JobInfo job, int uId, boolean persisted) {
-        this(job, uId, persisted, 0);
+    public JobStatus(JobInfo job, int uId) {
+        this(job, uId, 0);
 
         final long elapsedNow = SystemClock.elapsedRealtime();
 
@@ -105,7 +101,7 @@
      */
     public JobStatus(JobInfo job, int uId, long earliestRunTimeElapsedMillis,
                       long latestRunTimeElapsedMillis) {
-        this(job, uId, true, 0);
+        this(job, uId, 0);
 
         this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
         this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
@@ -114,7 +110,7 @@
     /** Create a new job to be rescheduled with the provided parameters. */
     public JobStatus(JobStatus rescheduling, long newEarliestRuntimeElapsedMillis,
                       long newLatestRuntimeElapsedMillis, int backoffAttempt) {
-        this(rescheduling.job, rescheduling.getUid(), rescheduling.isPersisted(), backoffAttempt);
+        this(rescheduling.job, rescheduling.getUid(), backoffAttempt);
 
         earliestRunTimeElapsedMillis = newEarliestRuntimeElapsedMillis;
         latestRunTimeElapsedMillis = newLatestRuntimeElapsedMillis;
@@ -172,6 +168,10 @@
         return job.isRequireDeviceIdle();
     }
 
+    public boolean isPersisted() {
+        return job.isPersisted();
+    }
+
     public long getEarliestRunTime() {
         return earliestRunTimeElapsedMillis;
     }
@@ -180,9 +180,6 @@
         return latestRunTimeElapsedMillis;
     }
 
-    public boolean isPersisted() {
-        return persisted;
-    }
     /**
      * @return Whether or not this job is ready to run, based on its requirements.
      */
diff --git a/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java b/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java
index 7a7fa07..cb8da70 100644
--- a/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/task/TaskStoreTest.java
@@ -63,8 +63,9 @@
                 .setBackoffCriteria(initialBackoff, JobInfo.BackoffPolicy.EXPONENTIAL)
                 .setOverrideDeadline(runByMillis)
                 .setMinimumLatency(runFromMillis)
+                .setIsPersisted(true)
                 .build();
-        final JobStatus ts = new JobStatus(task, SOME_UID, true /* persisted */);
+        final JobStatus ts = new JobStatus(task, SOME_UID);
         mTaskStoreUnderTest.add(ts);
         Thread.sleep(IO_WAIT);
         // Manually load tasks from xml file.
@@ -89,15 +90,17 @@
                 .setRequiresDeviceIdle(true)
                 .setPeriodic(10000L)
                 .setRequiresCharging(true)
+                .setIsPersisted(true)
                 .build();
         final JobInfo task2 = new Builder(12, mComponent)
                 .setMinimumLatency(5000L)
                 .setBackoffCriteria(15000L, JobInfo.BackoffPolicy.LINEAR)
                 .setOverrideDeadline(30000L)
                 .setRequiredNetworkCapabilities(JobInfo.NetworkType.UNMETERED)
+                .setIsPersisted(true)
                 .build();
-        final JobStatus taskStatus1 = new JobStatus(task1, SOME_UID, true /* persisted */);
-        final JobStatus taskStatus2 = new JobStatus(task2, SOME_UID, true /* persisted */);
+        final JobStatus taskStatus1 = new JobStatus(task1, SOME_UID);
+        final JobStatus taskStatus2 = new JobStatus(task2, SOME_UID);
         mTaskStoreUnderTest.add(taskStatus1);
         mTaskStoreUnderTest.add(taskStatus2);
         Thread.sleep(IO_WAIT);
@@ -128,7 +131,8 @@
         JobInfo.Builder b = new Builder(8, mComponent)
                 .setRequiresDeviceIdle(true)
                 .setPeriodic(10000L)
-                .setRequiresCharging(true);
+                .setRequiresCharging(true)
+                .setIsPersisted(true);
 
         PersistableBundle extras = new PersistableBundle();
         extras.putDouble("hello", 3.2);
@@ -136,7 +140,7 @@
         extras.putInt("into", 3);
         b.setExtras(extras);
         final JobInfo task = b.build();
-        JobStatus taskStatus = new JobStatus(task, SOME_UID, true /* persisted */);
+        JobStatus taskStatus = new JobStatus(task, SOME_UID);
 
         mTaskStoreUnderTest.add(taskStatus);
         Thread.sleep(IO_WAIT);