JobScheduler only run jobs for started users.

BUG: 12876556
Minor changes to test app to make persisting an option.
Change-Id: I1b40347878ec5ca44cd717ebfeb544f6c58473b5
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index b7af544..e4ed470e 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -26,6 +26,9 @@
  * 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 {
     public interface NetworkType {
@@ -434,7 +437,7 @@
          * @return The job object to hand to the JobScheduler. This object is immutable.
          */
         public JobInfo build() {
-            // Allow tasks with no constraints. What am I, a database?
+            // Allow jobs with no constraints - What am I, a database?
             if (!mHasEarlyConstraint && !mHasLateConstraint && !mRequiresCharging &&
                     !mRequiresDeviceIdle && mNetworkCapabilities == NetworkType.NONE) {
                 throw new IllegalArgumentException("You're trying to build a job with no " +
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 6771cce..60f880c 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -117,6 +117,8 @@
      */
     final ArrayList<JobStatus> mPendingJobs = new ArrayList<JobStatus>();
 
+    final ArrayList<Integer> mStartedUsers = new ArrayList();
+
     final JobHandler mHandler;
     final JobSchedulerStub mJobSchedulerStub;
 
@@ -151,6 +153,18 @@
         }
     };
 
+    @Override
+    public void onStartUser(int userHandle) {
+        mStartedUsers.add(userHandle);
+        // Let's kick any outstanding jobs for this user.
+        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+    }
+
+    @Override
+    public void onStopUser(int userHandle) {
+        mStartedUsers.remove(Integer.valueOf(userHandle));
+    }
+
     /**
      * Entry point from client to schedule the provided job.
      * This cancels the job if it's already been scheduled, and replaces it with the one provided.
@@ -610,9 +624,20 @@
          *      - It's ready.
          *      - It's not pending.
          *      - It's not already running on a JSC.
+         *      - The user that requested the job is running.
          */
         private boolean isReadyToBeExecutedLocked(JobStatus job) {
-              return job.isReady() && !mPendingJobs.contains(job) && !isCurrentlyActiveLocked(job);
+            final boolean jobReady = job.isReady();
+            final boolean jobPending = mPendingJobs.contains(job);
+            final boolean jobActive = isCurrentlyActiveLocked(job);
+            final boolean userRunning = mStartedUsers.contains(job.getUserId());
+
+            if (DEBUG) {
+                Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+                        + " ready=" + jobReady + " pending=" + jobPending
+                        + " active=" + jobActive + " userRunning=" + userRunning);
+            }
+            return userRunning && jobReady && !jobPending && !jobActive;
         }
 
         /**
@@ -795,6 +820,11 @@
 
     void dumpInternal(PrintWriter pw) {
         synchronized (mJobs) {
+            pw.print("Started users: ");
+            for (int i=0; i<mStartedUsers.size(); i++) {
+                pw.print("u" + mStartedUsers.get(i) + " ");
+            }
+            pw.println();
             pw.println("Registered jobs:");
             if (mJobs.size() > 0) {
                 ArraySet<JobStatus> jobs = mJobs.getJobs();
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 652d8f8..a257ea0 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -21,6 +21,7 @@
 import android.os.PersistableBundle;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.text.format.DateUtils;
 
 import java.io.PrintWriter;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -41,6 +42,7 @@
     public static final long NO_EARLIEST_RUNTIME = 0L;
 
     final JobInfo job;
+    /** Uid of the package requesting this job. */
     final int uId;
     final String name;
     final String tag;
@@ -214,12 +216,39 @@
         return String.valueOf(hashCode()).substring(0, 3) + ".."
                 + ":[" + job.getService()
                 + ",jId=" + job.getId()
-                + ",R=(" + earliestRunTimeElapsedMillis + "," + latestRunTimeElapsedMillis + ")"
+                + ",u" + getUserId()
+                + ",R=(" + formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME)
+                + "," + formatRunTime(latestRunTimeElapsedMillis, NO_LATEST_RUNTIME) + ")"
                 + ",N=" + job.getNetworkCapabilities() + ",C=" + job.isRequireCharging()
                 + ",I=" + job.isRequireDeviceIdle() + ",F=" + numFailures
+                + ",P=" + job.isPersisted()
                 + (isReady() ? "(READY)" : "")
                 + "]";
     }
+
+    private String formatRunTime(long runtime, long  defaultValue) {
+        if (runtime == defaultValue) {
+            return "none";
+        } else {
+            long elapsedNow = SystemClock.elapsedRealtime();
+            long nextRuntime = runtime - elapsedNow;
+            if (nextRuntime > 0) {
+                return DateUtils.formatElapsedTime(nextRuntime / 1000);
+            } else {
+                return "-" + DateUtils.formatElapsedTime(nextRuntime / -1000);
+            }
+        }
+    }
+
+    /**
+     * Convenience function to identify a job uniquely without pulling all the data that
+     * {@link #toString()} returns.
+     */
+    public String toShortString() {
+        return job.getService().flattenToShortString() + " jId=" + job.getId() +
+                ", u" + getUserId();
+    }
+
     // Dumpsys infrastructure
     public void dump(PrintWriter pw, String prefix) {
         pw.println(this.toString());
diff --git a/tests/JobSchedulerTestApp/res/layout/activity_main.xml b/tests/JobSchedulerTestApp/res/layout/activity_main.xml
index d3429ff..96e1641 100644
--- a/tests/JobSchedulerTestApp/res/layout/activity_main.xml
+++ b/tests/JobSchedulerTestApp/res/layout/activity_main.xml
@@ -141,6 +141,20 @@
                         android:id="@+id/checkbox_idle"
                         android:text="@string/idle_mode_text"/>
                 </LinearLayout>
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/persisted_caption"
+                        android:layout_marginRight="15dp"/>
+                    <CheckBox
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/checkbox_persisted"
+                        android:text="@string/persisted_mode_text"/>
+                </LinearLayout>
 
             </LinearLayout>
         <Button
diff --git a/tests/JobSchedulerTestApp/res/values/strings.xml b/tests/JobSchedulerTestApp/res/values/strings.xml
index eebfb19..90dd2b6 100644
--- a/tests/JobSchedulerTestApp/res/values/strings.xml
+++ b/tests/JobSchedulerTestApp/res/values/strings.xml
@@ -27,6 +27,7 @@
     <string name="charging_caption">Charging:</string>
     <string name="charging_text">Requires device plugged in.</string>
     <string name="idle_caption">Idle:</string>
+    <string name="persisted_caption">Persisted:</string>
     <string name="constraints">Constraints</string>
     <string name="connectivity">Connectivity:</string>
     <string name="any">Any</string>
@@ -34,4 +35,5 @@
     <string name="timing">Timing:</string>
     <string name="delay">Delay:</string>
     <string name="deadline">Deadline:</string>
+    <string name="persisted_mode_text">Persisted:</string>
 </resources>
diff --git a/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/MainActivity.java b/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/MainActivity.java
index e15929d..6e5484e 100644
--- a/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/MainActivity.java
+++ b/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/MainActivity.java
@@ -65,6 +65,8 @@
         mAnyConnectivityRadioButton = (RadioButton) findViewById(R.id.checkbox_any);
         mRequiresChargingCheckBox = (CheckBox) findViewById(R.id.checkbox_charging);
         mRequiresIdleCheckbox = (CheckBox) findViewById(R.id.checkbox_idle);
+        mIsPersistedCheckbox = (CheckBox) findViewById(R.id.checkbox_persisted);
+
         mServiceComponent = new ComponentName(this, TestJobService.class);
         // Start service and provide it a way to communicate with us.
         Intent startServiceIntent = new Intent(this, TestJobService.class);
@@ -85,6 +87,7 @@
     RadioButton mAnyConnectivityRadioButton;
     CheckBox mRequiresChargingCheckBox;
     CheckBox mRequiresIdleCheckbox;
+    CheckBox mIsPersistedCheckbox;
 
     ComponentName mServiceComponent;
     /** Service object to interact scheduled jobs. */
@@ -146,7 +149,7 @@
         }
         builder.setRequiresDeviceIdle(mRequiresIdleCheckbox.isChecked());
         builder.setRequiresCharging(mRequiresChargingCheckBox.isChecked());
-
+        builder.setIsPersisted(mIsPersistedCheckbox.isChecked());
         mTestService.scheduleJob(builder.build());
 
     }
diff --git a/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/service/TestJobService.java b/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/service/TestJobService.java
index e2c3be0..a68e04e 100644
--- a/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/service/TestJobService.java
+++ b/tests/JobSchedulerTestApp/src/com/android/demo/jobSchedulerApp/service/TestJobService.java
@@ -90,31 +90,6 @@
             mActivity.onReceivedStartJob(params);
         }
 
-        // Spin off a new task on a separate thread for a couple seconds.
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... voids) {
-                try {
-                    Log.d(TAG, "Sleeping for 3 seconds.");
-                    Thread.sleep(3000L);
-                } catch (InterruptedException e) {}
-                final JobParameters params = jobParamsMap.get(currId);
-                Log.d(TAG, "Pulled :" + currId + " " + params);
-                jobFinished(params, false);
-
-                Log.d(TAG, "Rescheduling new job: " + params.getJobId());
-                scheduleJob(
-                        new JobInfo.Builder(params.getJobId(),
-                                new ComponentName(getBaseContext(), TestJobService.class))
-                                .setMinimumLatency(2000L)
-                                .setOverrideDeadline(3000L)
-                                .setRequiresCharging(true)
-                                .build()
-                );
-
-                return null;
-            }
-        }.execute();
         return true;
     }