Work on issue #28942589: Tune job scheduler

We now have a new settings key that provides all of the existing
tuning parameters, plus some newly redone ones for dealing with
different memory levels.

Changed the minimum batching for overall jobs from 2 to 1, so
we will never get in the way of immediately scheduling jobs
when the developer asks for this.  We should now be able to rely
on the doze modes to do better batching of jobs for us when it
is really important.

Also work on issue #28981330: Excessive JobScheduler wakeup alarms.
Use a work source with scheduled alarms to blame them on the app
whose job they are being scheduled for, and add a check for whether
a job's timing constraint has been satisfied before considering it
a possible candidate for the next alarm.  (If it is satisified,
the time is in the past, so we should not schedule an alarm for it.)

Finally clean up a bunch of the dumpsys output to make it easier
to understand.

Change-Id: I06cf2c1310448f47cf386f393e9b267335fabaeb
diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java
index 02dcc5c..f9df4f4 100644
--- a/core/java/android/app/AlarmManager.java
+++ b/core/java/android/app/AlarmManager.java
@@ -606,6 +606,22 @@
      *
      * @hide
      */
+    public void set(int type, long triggerAtMillis, long windowMillis, long intervalMillis,
+            String tag, OnAlarmListener listener, Handler targetHandler, WorkSource workSource) {
+        setImpl(type, triggerAtMillis, windowMillis, intervalMillis, 0, null, listener, tag,
+                targetHandler, workSource, null);
+    }
+
+    /**
+     * Direct callback version of {@link #set(int, long, long, long, PendingIntent, WorkSource)}.
+     * Note that repeating alarms must use the PendingIntent variant, not an OnAlarmListener.
+     * <p>
+     * The OnAlarmListener's {@link OnAlarmListener#onAlarm() onAlarm()} method will be
+     * invoked via the specified target Handler, or on the application's main looper
+     * if {@code null} is passed as the {@code targetHandler} parameter.
+     *
+     * @hide
+     */
     @SystemApi
     public void set(int type, long triggerAtMillis, long windowMillis, long intervalMillis,
             OnAlarmListener listener, Handler targetHandler, WorkSource workSource) {
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 61790ea..63c1dbb 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -192,7 +192,8 @@
     private final int flags;
 
     /**
-     * Unique job id associated with this class. This is assigned to your job by the scheduler.
+     * 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;
@@ -524,9 +525,9 @@
 
     /** Builder class for constructing {@link JobInfo} objects. */
     public static final class Builder {
-        private int mJobId;
+        private final int mJobId;
+        private final ComponentName mJobService;
         private PersistableBundle mExtras = PersistableBundle.EMPTY;
-        private ComponentName mJobService;
         private int mPriority = PRIORITY_DEFAULT;
         private int mFlags;
         // Requirements.
@@ -553,11 +554,15 @@
         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.
+         * 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.
+         * JobScheduler.
          */
         public Builder(int jobId, ComponentName jobService) {
             mJobService = jobService;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f867fb0..e408cca 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -8018,6 +8018,36 @@
         public static final String ALARM_MANAGER_CONSTANTS = "alarm_manager_constants";
 
         /**
+         * Job scheduler specific settings.
+         * This is encoded as a key=value list, separated by commas. Ex:
+         *
+         * "min_ready_jobs_count=2,moderate_use_factor=.5"
+         *
+         * The following keys are supported:
+         *
+         * <pre>
+         * min_idle_count                       (int)
+         * min_charging_count                   (int)
+         * min_connectivity_count               (int)
+         * min_content_count                    (int)
+         * min_ready_jobs_count                 (int)
+         * heavy_use_factor                     (float)
+         * moderate_use_factor                  (float)
+         * fg_job_count                         (int)
+         * bg_normal_job_count                  (int)
+         * bg_moderate_job_count                (int)
+         * bg_low_job_count                     (int)
+         * bg_critical_job_count                (int)
+         * </pre>
+         *
+         * <p>
+         * Type: string
+         * @hide
+         * @see com.android.server.job.JobSchedulerService.Constants
+         */
+        public static final String JOB_SCHEDULER_CONSTANTS = "job_scheduler_constants";
+
+        /**
          * ShortcutManager specific settings.
          * This is encoded as a key=value list, separated by commas. Ex:
          *
diff --git a/core/java/android/util/KeyValueListParser.java b/core/java/android/util/KeyValueListParser.java
index 4abdde0..e4c025d 100644
--- a/core/java/android/util/KeyValueListParser.java
+++ b/core/java/android/util/KeyValueListParser.java
@@ -63,6 +63,24 @@
     }
 
     /**
+     * Get the value for key as an int.
+     * @param key The key to lookup.
+     * @param def The value to return if the key was not found, or the value was not a long.
+     * @return the int value associated with the key.
+     */
+    public int getInt(String key, int def) {
+        String value = mValues.get(key);
+        if (value != null) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                // fallthrough
+            }
+        }
+        return def;
+    }
+
+    /**
      * Get the value for key as a long.
      * @param key The key to lookup.
      * @param def The value to return if the key was not found, or the value was not a long.
diff --git a/services/core/java/com/android/server/job/JobPackageTracker.java b/services/core/java/com/android/server/job/JobPackageTracker.java
index eb5edd3..ba96b74 100644
--- a/services/core/java/com/android/server/job/JobPackageTracker.java
+++ b/services/core/java/com/android/server/job/JobPackageTracker.java
@@ -105,6 +105,8 @@
         final long mStartElapsedTime;
         final long mStartClockTime;
         long mSummedTime;
+        int mMaxTotalActive;
+        int mMaxFgActive;
 
         public DataSet(DataSet otherTimes) {
             mStartUptimeTime = otherTimes.mStartUptimeTime;
@@ -257,6 +259,12 @@
                     }
                 }
             }
+            if (mMaxTotalActive > out.mMaxTotalActive) {
+                out.mMaxTotalActive = mMaxTotalActive;
+            }
+            if (mMaxFgActive > out.mMaxFgActive) {
+                out.mMaxFgActive = mMaxFgActive;
+            }
         }
 
         void printDuration(PrintWriter pw, long period, long duration, int count, String suffix) {
@@ -317,6 +325,9 @@
                     pw.println();
                 }
             }
+            pw.print(prefix); pw.print("  Max concurrency: ");
+            pw.print(mMaxTotalActive); pw.print(" total, ");
+            pw.print(mMaxFgActive); pw.println(" foreground");
         }
     }
 
@@ -366,6 +377,15 @@
         addEvent(EVENT_STOP_JOB, job.getSourceUid(), job.getBatteryName());
     }
 
+    public void noteConcurrency(int totalActive, int fgActive) {
+        if (totalActive > mCurDataSet.mMaxTotalActive) {
+            mCurDataSet.mMaxTotalActive = totalActive;
+        }
+        if (fgActive > mCurDataSet.mMaxFgActive) {
+            mCurDataSet.mMaxFgActive = fgActive;
+        }
+    }
+
     public float getLoadFactor(JobStatus job) {
         final int uid = job.getSourceUid();
         final String pkg = job.getSourcePackageName();
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 1b8eccb..5e212b0 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -23,6 +23,8 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 
@@ -37,6 +39,7 @@
 import android.app.job.IJobScheduler;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -44,6 +47,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.BatteryStats;
 import android.os.Binder;
@@ -57,6 +61,8 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -98,17 +104,12 @@
     public static final boolean DEBUG = false;
 
     /** The maximum number of concurrent jobs we run at one time. */
-    private static final int MAX_JOB_CONTEXTS_COUNT = 12;
-    /** The number of MAX_JOB_CONTEXTS_COUNT we reserve for the foreground app. */
-    private static final int FG_JOB_CONTEXTS_COUNT = 4;
+    private static final int MAX_JOB_CONTEXTS_COUNT = 16;
     /** Enforce a per-app limit on scheduled jobs? */
     private static final boolean ENFORCE_MAX_JOBS = true;
     /** The maximum number of jobs that we allow an unprivileged app to schedule */
     private static final int MAX_JOBS_PER_APP = 100;
-    /** This is the job execution factor that is considered to be heavy use of the system. */
-    private static final float HEAVY_USE_FACTOR = .9f;
-    /** This is the job execution factor that is considered to be moderate use of the system. */
-    private static final float MODERATE_USE_FACTOR = .5f;
+
 
     /** Global local for all job scheduler state. */
     final Object mLock = new Object();
@@ -122,34 +123,6 @@
     static final int MSG_STOP_JOB = 2;
     static final int MSG_CHECK_JOB_GREEDY = 3;
 
-    // Policy constants
-    /**
-     * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
-     * early.
-     */
-    static final int MIN_IDLE_COUNT = 1;
-    /**
-     * Minimum # of charging jobs that must be ready in order to force the JMS to schedule things
-     * early.
-     */
-    static final int MIN_CHARGING_COUNT = 1;
-    /**
-     * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
-     * things early.
-     */
-    static final int MIN_CONNECTIVITY_COUNT = 1;  // Run connectivity jobs as soon as ready.
-    /**
-     * Minimum # of content trigger jobs that must be ready in order to force the JMS to schedule
-     * things early.
-     */
-    static final int MIN_CONTENT_COUNT = 1;
-    /**
-     * Minimum # of jobs (with no particular constraints) for which the JMS will be happy running
-     * some work early.
-     * This is correlated with the amount of batching we'll be able to do.
-     */
-    static final int MIN_READY_JOBS_COUNT = 2;
-
     /**
      * Track Services that have currently active or pending jobs. The index is provided by
      * {@link JobStatus#getServiceToken()}
@@ -186,7 +159,7 @@
      * Current limit on the number of concurrent JobServiceContext entries we want to
      * keep actively running a job.
      */
-    int mMaxActiveJobs = MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT;
+    int mMaxActiveJobs = 1;
 
     /**
      * Which uids are currently in the foreground.
@@ -212,6 +185,211 @@
     int[] mTmpAssignPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT];
 
     /**
+     * All times are in milliseconds. These constants are kept synchronized with the system
+     * global Settings. Any access to this class or its fields should be done while
+     * holding the JobSchedulerService.mLock lock.
+     */
+    private final class Constants extends ContentObserver {
+        // Key names stored in the settings value.
+        private static final String KEY_MIN_IDLE_COUNT = "min_idle_count";
+        private static final String KEY_MIN_CHARGING_COUNT = "min_charging_count";
+        private static final String KEY_MIN_CONNECTIVITY_COUNT = "min_connectivity_count";
+        private static final String KEY_MIN_CONTENT_COUNT = "min_content_count";
+        private static final String KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count";
+        private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor";
+        private static final String KEY_MODERATE_USE_FACTOR = "moderate_use_factor";
+        private static final String KEY_FG_JOB_COUNT = "fg_job_count";
+        private static final String KEY_BG_NORMAL_JOB_COUNT = "bg_normal_job_count";
+        private static final String KEY_BG_MODERATE_JOB_COUNT = "bg_moderate_job_count";
+        private static final String KEY_BG_LOW_JOB_COUNT = "bg_low_job_count";
+        private static final String KEY_BG_CRITICAL_JOB_COUNT = "bg_critical_job_count";
+
+        private static final int DEFAULT_MIN_IDLE_COUNT = 1;
+        private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
+        private static final int DEFAULT_MIN_CONNECTIVITY_COUNT = 1;
+        private static final int DEFAULT_MIN_CONTENT_COUNT = 1;
+        private static final int DEFAULT_MIN_READY_JOBS_COUNT = 1;
+        private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
+        private static final float DEFAULT_MODERATE_USE_FACTOR = .5f;
+        private static final int DEFAULT_FG_JOB_COUNT = 4;
+        private static final int DEFAULT_BG_NORMAL_JOB_COUNT = 6;
+        private static final int DEFAULT_BG_MODERATE_JOB_COUNT = 4;
+        private static final int DEFAULT_BG_LOW_JOB_COUNT = 2;
+        private static final int DEFAULT_BG_CRITICAL_JOB_COUNT = 1;
+
+        /**
+         * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
+         * early.
+         */
+        int MIN_IDLE_COUNT = DEFAULT_MIN_IDLE_COUNT;
+        /**
+         * Minimum # of charging jobs that must be ready in order to force the JMS to schedule
+         * things early.
+         */
+        int MIN_CHARGING_COUNT = DEFAULT_MIN_CHARGING_COUNT;
+        /**
+         * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
+         * things early.  1 == Run connectivity jobs as soon as ready.
+         */
+        int MIN_CONNECTIVITY_COUNT = DEFAULT_MIN_CONNECTIVITY_COUNT;
+        /**
+         * Minimum # of content trigger jobs that must be ready in order to force the JMS to
+         * schedule things early.
+         */
+        int MIN_CONTENT_COUNT = DEFAULT_MIN_CONTENT_COUNT;
+        /**
+         * Minimum # of jobs (with no particular constraints) for which the JMS will be happy
+         * running some work early.  This (and thus the other min counts) is now set to 1, to
+         * prevent any batching at this level.  Since we now do batching through doze, that is
+         * a much better mechanism.
+         */
+        int MIN_READY_JOBS_COUNT = DEFAULT_MIN_READY_JOBS_COUNT;
+        /**
+         * This is the job execution factor that is considered to be heavy use of the system.
+         */
+        float HEAVY_USE_FACTOR = DEFAULT_HEAVY_USE_FACTOR;
+        /**
+         * This is the job execution factor that is considered to be moderate use of the system.
+         */
+        float MODERATE_USE_FACTOR = DEFAULT_MODERATE_USE_FACTOR;
+        /**
+         * The number of MAX_JOB_CONTEXTS_COUNT we reserve for the foreground app.
+         */
+        int FG_JOB_COUNT = DEFAULT_FG_JOB_COUNT;
+        /**
+         * The maximum number of background jobs we allow when the system is in a normal
+         * memory state.
+         */
+        int BG_NORMAL_JOB_COUNT = DEFAULT_BG_NORMAL_JOB_COUNT;
+        /**
+         * The maximum number of background jobs we allow when the system is in a moderate
+         * memory state.
+         */
+        int BG_MODERATE_JOB_COUNT = DEFAULT_BG_MODERATE_JOB_COUNT;
+        /**
+         * The maximum number of background jobs we allow when the system is in a low
+         * memory state.
+         */
+        int BG_LOW_JOB_COUNT = DEFAULT_BG_LOW_JOB_COUNT;
+        /**
+         * The maximum number of background jobs we allow when the system is in a critical
+         * memory state.
+         */
+        int BG_CRITICAL_JOB_COUNT = DEFAULT_BG_CRITICAL_JOB_COUNT;
+
+        private ContentResolver mResolver;
+        private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+        public Constants(Handler handler) {
+            super(handler);
+        }
+
+        public void start(ContentResolver resolver) {
+            mResolver = resolver;
+            mResolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.JOB_SCHEDULER_CONSTANTS), false, this);
+            updateConstants();
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            updateConstants();
+        }
+
+        private void updateConstants() {
+            synchronized (mLock) {
+                try {
+                    mParser.setString(Settings.Global.getString(mResolver,
+                            Settings.Global.ALARM_MANAGER_CONSTANTS));
+                } catch (IllegalArgumentException e) {
+                    // Failed to parse the settings string, log this and move on
+                    // with defaults.
+                    Slog.e(TAG, "Bad device idle settings", e);
+                }
+
+                MIN_IDLE_COUNT = mParser.getInt(KEY_MIN_IDLE_COUNT,
+                        DEFAULT_MIN_IDLE_COUNT);
+                MIN_CHARGING_COUNT = mParser.getInt(KEY_MIN_CHARGING_COUNT,
+                        DEFAULT_MIN_CHARGING_COUNT);
+                MIN_CONNECTIVITY_COUNT = mParser.getInt(KEY_MIN_CONNECTIVITY_COUNT,
+                        DEFAULT_MIN_CONNECTIVITY_COUNT);
+                MIN_CONTENT_COUNT = mParser.getInt(KEY_MIN_CONTENT_COUNT,
+                        DEFAULT_MIN_CONTENT_COUNT);
+                MIN_READY_JOBS_COUNT = mParser.getInt(KEY_MIN_READY_JOBS_COUNT,
+                        DEFAULT_MIN_READY_JOBS_COUNT);
+                HEAVY_USE_FACTOR = mParser.getFloat(KEY_HEAVY_USE_FACTOR,
+                        DEFAULT_HEAVY_USE_FACTOR);
+                MODERATE_USE_FACTOR = mParser.getFloat(KEY_MODERATE_USE_FACTOR,
+                        DEFAULT_MODERATE_USE_FACTOR);
+                FG_JOB_COUNT = mParser.getInt(KEY_FG_JOB_COUNT,
+                        DEFAULT_FG_JOB_COUNT);
+                BG_NORMAL_JOB_COUNT = mParser.getInt(KEY_BG_NORMAL_JOB_COUNT,
+                        DEFAULT_BG_NORMAL_JOB_COUNT);
+                if ((FG_JOB_COUNT+BG_NORMAL_JOB_COUNT) > MAX_JOB_CONTEXTS_COUNT) {
+                    BG_NORMAL_JOB_COUNT = MAX_JOB_CONTEXTS_COUNT - FG_JOB_COUNT;
+                }
+                BG_MODERATE_JOB_COUNT = mParser.getInt(KEY_BG_MODERATE_JOB_COUNT,
+                        DEFAULT_BG_MODERATE_JOB_COUNT);
+                if ((FG_JOB_COUNT+BG_MODERATE_JOB_COUNT) > MAX_JOB_CONTEXTS_COUNT) {
+                    BG_MODERATE_JOB_COUNT = MAX_JOB_CONTEXTS_COUNT - FG_JOB_COUNT;
+                }
+                BG_LOW_JOB_COUNT = mParser.getInt(KEY_BG_LOW_JOB_COUNT,
+                        DEFAULT_BG_LOW_JOB_COUNT);
+                if ((FG_JOB_COUNT+BG_LOW_JOB_COUNT) > MAX_JOB_CONTEXTS_COUNT) {
+                    BG_LOW_JOB_COUNT = MAX_JOB_CONTEXTS_COUNT - FG_JOB_COUNT;
+                }
+                BG_CRITICAL_JOB_COUNT = mParser.getInt(KEY_BG_CRITICAL_JOB_COUNT,
+                        DEFAULT_BG_CRITICAL_JOB_COUNT);
+                if ((FG_JOB_COUNT+BG_CRITICAL_JOB_COUNT) > MAX_JOB_CONTEXTS_COUNT) {
+                    BG_CRITICAL_JOB_COUNT = MAX_JOB_CONTEXTS_COUNT - FG_JOB_COUNT;
+                }
+            }
+        }
+
+        void dump(PrintWriter pw) {
+            pw.println("  Settings:");
+
+            pw.print("    "); pw.print(KEY_MIN_IDLE_COUNT); pw.print("=");
+            pw.print(MIN_IDLE_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_MIN_CHARGING_COUNT); pw.print("=");
+            pw.print(MIN_CHARGING_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_MIN_CONNECTIVITY_COUNT); pw.print("=");
+            pw.print(MIN_CONNECTIVITY_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_MIN_CONTENT_COUNT); pw.print("=");
+            pw.print(MIN_CONTENT_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_MIN_READY_JOBS_COUNT); pw.print("=");
+            pw.print(MIN_READY_JOBS_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_HEAVY_USE_FACTOR); pw.print("=");
+            pw.print(HEAVY_USE_FACTOR); pw.println();
+
+            pw.print("    "); pw.print(KEY_MODERATE_USE_FACTOR); pw.print("=");
+            pw.print(MODERATE_USE_FACTOR); pw.println();
+
+            pw.print("    "); pw.print(KEY_FG_JOB_COUNT); pw.print("=");
+            pw.print(FG_JOB_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_BG_NORMAL_JOB_COUNT); pw.print("=");
+            pw.print(BG_NORMAL_JOB_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_BG_MODERATE_JOB_COUNT); pw.print("=");
+            pw.print(BG_MODERATE_JOB_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_BG_LOW_JOB_COUNT); pw.print("=");
+            pw.print(BG_LOW_JOB_COUNT); pw.println();
+
+            pw.print("    "); pw.print(KEY_BG_CRITICAL_JOB_COUNT); pw.print("=");
+            pw.print(BG_CRITICAL_JOB_COUNT); pw.println();
+        }
+    }
+
+    final Constants mConstants;
+
+    /**
      * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we
      * still clean up. On reinstall the package will have a new uid.
      */
@@ -550,6 +728,7 @@
         mControllers.add(DeviceIdleJobsController.get(this));
 
         mHandler = new JobHandler(context.getMainLooper());
+        mConstants = new Constants(mHandler);
         mJobSchedulerStub = new JobSchedulerStub();
         mJobs = JobStore.initAndGet(this);
     }
@@ -563,6 +742,7 @@
     @Override
     public void onBootPhase(int phase) {
         if (PHASE_SYSTEM_SERVICES_READY == phase) {
+            mConstants.start(getContext().getContentResolver());
             // Register br for package removals and user removals.
             final IntentFilter filter = new IntentFilter();
             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
@@ -994,11 +1174,12 @@
 
             public void postProcess() {
                 if (backoffCount > 0 ||
-                        idleCount >= MIN_IDLE_COUNT ||
-                        connectivityCount >= MIN_CONNECTIVITY_COUNT ||
-                        chargingCount >= MIN_CHARGING_COUNT ||
-                        contentCount  >= MIN_CONTENT_COUNT ||
-                        (runnableJobs != null && runnableJobs.size() >= MIN_READY_JOBS_COUNT)) {
+                        idleCount >= mConstants.MIN_IDLE_COUNT ||
+                        connectivityCount >= mConstants.MIN_CONNECTIVITY_COUNT ||
+                        chargingCount >= mConstants.MIN_CHARGING_COUNT ||
+                        contentCount >= mConstants.MIN_CONTENT_COUNT ||
+                        (runnableJobs != null
+                                && runnableJobs.size() >= mConstants.MIN_READY_JOBS_COUNT)) {
                     if (DEBUG) {
                         Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Running jobs.");
                     }
@@ -1095,9 +1276,9 @@
     private int adjustJobPriority(int curPriority, JobStatus job) {
         if (curPriority < JobInfo.PRIORITY_TOP_APP) {
             float factor = mJobPackageTracker.getLoadFactor(job);
-            if (factor >= HEAVY_USE_FACTOR) {
+            if (factor >= mConstants.HEAVY_USE_FACTOR) {
                 curPriority += JobInfo.PRIORITY_ADJ_ALWAYS_RUNNING;
-            } else if (factor >= MODERATE_USE_FACTOR) {
+            } else if (factor >= mConstants.MODERATE_USE_FACTOR) {
                 curPriority += JobInfo.PRIORITY_ADJ_OFTEN_RUNNING;
             }
         }
@@ -1135,16 +1316,16 @@
         }
         switch (memLevel) {
             case ProcessStats.ADJ_MEM_FACTOR_MODERATE:
-                mMaxActiveJobs = ((MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT) * 2) / 3;
+                mMaxActiveJobs = mConstants.BG_MODERATE_JOB_COUNT;
                 break;
             case ProcessStats.ADJ_MEM_FACTOR_LOW:
-                mMaxActiveJobs = (MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT) / 3;
+                mMaxActiveJobs = mConstants.BG_LOW_JOB_COUNT;
                 break;
             case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
-                mMaxActiveJobs = 1;
+                mMaxActiveJobs = mConstants.BG_CRITICAL_JOB_COUNT;
                 break;
             default:
-                mMaxActiveJobs = MAX_JOB_CONTEXTS_COUNT - FG_JOB_CONTEXTS_COUNT;
+                mMaxActiveJobs = mConstants.BG_NORMAL_JOB_COUNT;
                 break;
         }
 
@@ -1152,10 +1333,15 @@
         boolean[] act = mTmpAssignAct;
         int[] preferredUidForContext = mTmpAssignPreferredUidForContext;
         int numActive = 0;
+        int numForeground = 0;
         for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
             final JobServiceContext js = mActiveServices.get(i);
-            if ((contextIdToJobMap[i] = js.getRunningJob()) != null) {
+            final JobStatus status = js.getRunningJob();
+            if ((contextIdToJobMap[i] = status) != null) {
                 numActive++;
+                if (status.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) {
+                    numForeground++;
+                }
             }
             act[i] = false;
             preferredUidForContext[i] = js.getPreferredUid();
@@ -1184,13 +1370,14 @@
                 JobStatus job = contextIdToJobMap[j];
                 int preferredUid = preferredUidForContext[j];
                 if (job == null) {
-                    if ((numActive < mMaxActiveJobs || priority >= JobInfo.PRIORITY_TOP_APP) &&
+                    if ((numActive < mMaxActiveJobs ||
+                            (priority >= JobInfo.PRIORITY_TOP_APP &&
+                                    numForeground < mConstants.FG_JOB_COUNT)) &&
                             (preferredUid == nextPending.getUid() ||
                                     preferredUid == JobServiceContext.NO_PREFERRED_UID)) {
                         // This slot is free, and we haven't yet hit the limit on
                         // concurrent jobs...  we can just throw the job in to here.
                         minPriorityContextId = j;
-                        numActive++;
                         break;
                     }
                     // No job on this context, but nextPending can't run here because
@@ -1212,11 +1399,16 @@
             if (minPriorityContextId != -1) {
                 contextIdToJobMap[minPriorityContextId] = nextPending;
                 act[minPriorityContextId] = true;
+                numActive++;
+                if (priority >= JobInfo.PRIORITY_TOP_APP) {
+                    numForeground++;
+                }
             }
         }
         if (DEBUG) {
             Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final"));
         }
+        mJobPackageTracker.noteConcurrency(numActive, numForeground);
         for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
             boolean preservePreferredUid = false;
             if (act[i]) {
@@ -1569,36 +1761,49 @@
         final int filterUidFinal = UserHandle.getAppId(filterUid);
         final long now = SystemClock.elapsedRealtime();
         synchronized (mLock) {
+            mConstants.dump(pw);
+            pw.println();
             pw.println("Started users: " + Arrays.toString(mStartedUsers));
-            pw.println("Registered jobs:");
+            pw.print("Registered ");
+            pw.print(mJobs.size());
+            pw.println(" jobs:");
             if (mJobs.size() > 0) {
-                mJobs.forEachJob(new JobStatusFunctor() {
-                    private int index = 0;
-
+                final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs();
+                Collections.sort(jobs, new Comparator<JobStatus>() {
                     @Override
-                    public void process(JobStatus job) {
-                        pw.print("  Job #"); pw.print(index++); pw.print(": ");
-                        pw.println(job.toShortString());
-
-                        // Skip printing details if the caller requested a filter
-                        if (!job.shouldDump(filterUidFinal)) {
-                            return;
+                    public int compare(JobStatus o1, JobStatus o2) {
+                        int uid1 = o1.getUid();
+                        int uid2 = o2.getUid();
+                        int id1 = o1.getJobId();
+                        int id2 = o2.getJobId();
+                        if (uid1 != uid2) {
+                            return uid1 < uid2 ? -1 : 1;
                         }
-
-                        job.dump(pw, "    ", true);
-                        pw.print("    Ready: ");
-                        pw.print(mHandler.isReadyToBeExecutedLocked(job));
-                        pw.print(" (job=");
-                        pw.print(job.isReady());
-                        pw.print(" pending=");
-                        pw.print(mPendingJobs.contains(job));
-                        pw.print(" active=");
-                        pw.print(isCurrentlyActiveLocked(job));
-                        pw.print(" user=");
-                        pw.print(ArrayUtils.contains(mStartedUsers, job.getUserId()));
-                        pw.println(")");
+                        return id1 < id2 ? -1 : (id1 > id2 ? 1 : 0);
                     }
                 });
+                for (JobStatus job : jobs) {
+                    pw.print("  JOB #"); job.printUniqueId(pw); pw.print(": ");
+                    pw.println(job.toShortStringExceptUniqueId());
+
+                    // Skip printing details if the caller requested a filter
+                    if (!job.shouldDump(filterUidFinal)) {
+                        continue;
+                    }
+
+                    job.dump(pw, "    ", true);
+                    pw.print("    Ready: ");
+                    pw.print(mHandler.isReadyToBeExecutedLocked(job));
+                    pw.print(" (job=");
+                    pw.print(job.isReady());
+                    pw.print(" pending=");
+                    pw.print(mPendingJobs.contains(job));
+                    pw.print(" active=");
+                    pw.print(isCurrentlyActiveLocked(job));
+                    pw.print(" user=");
+                    pw.print(ArrayUtils.contains(mStartedUsers, job.getUserId()));
+                    pw.println(")");
+                }
             } else {
                 pw.println("  None.");
             }
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index 1f7d312..602b9c7 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -844,8 +844,16 @@
         // Inefficient; use only for testing
         public List<JobStatus> getAllJobs() {
             ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
-            for (int i = mJobs.size(); i >= 0; i--) {
-                allJobs.addAll(mJobs.valueAt(i));
+            for (int i = mJobs.size() - 1; i >= 0; i--) {
+                ArraySet<JobStatus> jobs = mJobs.valueAt(i);
+                if (jobs != null) {
+                    // Use a for loop over the ArraySet, so we don't need to make its
+                    // optional collection class iterator implementation or have to go
+                    // through a temporary array from toArray().
+                    for (int j = jobs.size() - 1; j >= 0; j--) {
+                        allJobs.add(jobs.valueAt(j));
+                    }
+                }
             }
             return allJobs;
         }
diff --git a/services/core/java/com/android/server/job/controllers/AppIdleController.java b/services/core/java/com/android/server/job/controllers/AppIdleController.java
index 7593035..a23af35 100644
--- a/services/core/java/com/android/server/job/controllers/AppIdleController.java
+++ b/services/core/java/com/android/server/job/controllers/AppIdleController.java
@@ -18,6 +18,7 @@
 
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
+import android.os.UserHandle;
 import android.util.Slog;
 
 import com.android.server.LocalServices;
@@ -42,6 +43,7 @@
     private static volatile AppIdleController sController;
     private final JobSchedulerService mJobSchedulerService;
     private final UsageStatsManagerInternal mUsageStatsInternal;
+    private boolean mInitializedParoleOn;
     boolean mAppIdleParoleOn;
 
     final class GlobalUpdateFunc implements JobStore.JobStatusFunctor {
@@ -100,12 +102,16 @@
         super(service, context, lock);
         mJobSchedulerService = service;
         mUsageStatsInternal = LocalServices.getService(UsageStatsManagerInternal.class);
-        mAppIdleParoleOn = mUsageStatsInternal.isAppIdleParoleOn();
+        mAppIdleParoleOn = true;
         mUsageStatsInternal.addAppIdleStateChangeListener(new AppIdleStateChangeListener());
     }
 
     @Override
     public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+        if (!mInitializedParoleOn) {
+            mInitializedParoleOn = true;
+            mAppIdleParoleOn = mUsageStatsInternal.isAppIdleParoleOn();
+        }
         String packageName = jobStatus.getSourcePackageName();
         final boolean appIdle = !mAppIdleParoleOn && mUsageStatsInternal.isAppIdle(packageName,
                 jobStatus.getSourceUid(), jobStatus.getSourceUserId());
@@ -122,21 +128,24 @@
 
     @Override
     public void dumpControllerStateLocked(final PrintWriter pw, final int filterUid) {
-        pw.println("AppIdle");
-        pw.println("Parole On: " + mAppIdleParoleOn);
+        pw.print("AppIdle: parole on = ");
+        pw.println(mAppIdleParoleOn);
         mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() {
             @Override public void process(JobStatus jobStatus) {
                 // Skip printing details if the caller requested a filter
                 if (!jobStatus.shouldDump(filterUid)) {
                     return;
                 }
-                pw.print("  ");
+                pw.print("  #");
+                jobStatus.printUniqueId(pw);
+                pw.print(" from ");
+                UserHandle.formatUid(pw, jobStatus.getSourceUid());
+                pw.print(": ");
                 pw.print(jobStatus.getSourcePackageName());
-                pw.print(": runnable=");
+                pw.print(", runnable=");
                 pw.println((jobStatus.satisfiedConstraints&JobStatus.CONSTRAINT_APP_NOT_IDLE) != 0);
             }
         });
-        pw.println();
     }
 
     void setAppIdleParoleOn(boolean isAppIdleParoleOn) {
diff --git a/services/core/java/com/android/server/job/controllers/BatteryController.java b/services/core/java/com/android/server/job/controllers/BatteryController.java
index a0cb25f..f6b8ef4 100644
--- a/services/core/java/com/android/server/job/controllers/BatteryController.java
+++ b/services/core/java/com/android/server/job/controllers/BatteryController.java
@@ -23,6 +23,7 @@
 import android.os.BatteryManager;
 import android.os.BatteryManagerInternal;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -195,21 +196,21 @@
 
     @Override
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
-        pw.println("Batt.");
-        pw.println("Stable power: " + mChargeTracker.isOnStablePower());
-        Iterator<JobStatus> it = mTrackedTasks.iterator();
-        if (it.hasNext()) {
-            JobStatus jobStatus = it.next();
-            if (jobStatus.shouldDump(filterUid)) {
-                pw.print(String.valueOf(jobStatus.hashCode()));
+        pw.print("Battery: stable power = ");
+        pw.println(mChargeTracker.isOnStablePower());
+        pw.print("Tracking ");
+        pw.print(mTrackedTasks.size());
+        pw.println(":");
+        for (int i = 0; i < mTrackedTasks.size(); i++) {
+            final JobStatus js = mTrackedTasks.get(i);
+            if (!js.shouldDump(filterUid)) {
+                continue;
             }
+            pw.print("  #");
+            js.printUniqueId(pw);
+            pw.print(" from ");
+            UserHandle.formatUid(pw, js.getSourceUid());
+            pw.println();
         }
-        while (it.hasNext()) {
-            JobStatus jobStatus = it.next();
-            if (jobStatus.shouldDump(filterUid)) {
-                pw.print("," + String.valueOf(jobStatus.hashCode()));
-            }
-        }
-        pw.println();
     }
 }
diff --git a/services/core/java/com/android/server/job/controllers/ConnectivityController.java b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
index 7d28633..2ff880c 100644
--- a/services/core/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/services/core/java/com/android/server/job/controllers/ConnectivityController.java
@@ -187,14 +187,20 @@
 
     @Override
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
-        pw.println("Conn.");
+        pw.println("Connectivity.");
+        pw.print("Tracking ");
+        pw.print(mTrackedJobs.size());
+        pw.println(":");
         for (int i = 0; i < mTrackedJobs.size(); i++) {
             final JobStatus js = mTrackedJobs.get(i);
             if (js.shouldDump(filterUid)) {
-                pw.println(String.valueOf(js.getJobId() + "," + js.getUid())
-                        + ": C=" + js.hasConnectivityConstraint()
-                        + ", UM=" + js.hasUnmeteredConstraint()
-                        + ", NR=" + js.hasNotRoamingConstraint());
+                pw.print("  #");
+                js.printUniqueId(pw);
+                pw.print(" from ");
+                UserHandle.formatUid(pw, js.getSourceUid());
+                pw.print(": C="); pw.print(js.hasConnectivityConstraint());
+                pw.print(": UM="); pw.print(js.hasUnmeteredConstraint());
+                pw.print(": NR="); pw.println(js.hasNotRoamingConstraint());
             }
         }
     }
diff --git a/services/core/java/com/android/server/job/controllers/ContentObserverController.java b/services/core/java/com/android/server/job/controllers/ContentObserverController.java
index c1d7c58..26660e8 100644
--- a/services/core/java/com/android/server/job/controllers/ContentObserverController.java
+++ b/services/core/java/com/android/server/job/controllers/ContentObserverController.java
@@ -21,6 +21,7 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.UserHandle;
 import android.util.TimeUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -323,23 +324,17 @@
 
     @Override
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
-        pw.println("Content.");
-        boolean printed = false;
+        pw.println("Content:");
         Iterator<JobStatus> it = mTrackedTasks.iterator();
         while (it.hasNext()) {
             JobStatus js = it.next();
             if (!js.shouldDump(filterUid)) {
                 continue;
             }
-            if (!printed) {
-                pw.print("  ");
-                printed = true;
-            } else {
-                pw.print(",");
-            }
-            pw.print(System.identityHashCode(js));
-        }
-        if (printed) {
+            pw.print("  #");
+            js.printUniqueId(pw);
+            pw.print(" from ");
+            UserHandle.formatUid(pw, js.getSourceUid());
             pw.println();
         }
         int N = mObservers.size();
@@ -367,8 +362,10 @@
                 pw.println("      Jobs:");
                 for (int j=0; j<M; j++) {
                     JobInstance inst = obs.mJobs.valueAt(j);
-                    pw.print("        ");
-                    pw.print(System.identityHashCode(inst.mJobStatus));
+                    pw.print("        #");
+                    inst.mJobStatus.printUniqueId(pw);
+                    pw.print(" from ");
+                    UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid());
                     if (inst.mChangedAuthorities != null) {
                         pw.println(":");
                         if (inst.mTriggerPending) {
diff --git a/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index 345a032..bf1297f 100644
--- a/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/services/core/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -180,13 +180,16 @@
                 if (!jobStatus.shouldDump(filterUid)) {
                     return;
                 }
-                pw.print("  ");
+                pw.print("  #");
+                jobStatus.printUniqueId(pw);
+                pw.print(" from ");
+                UserHandle.formatUid(pw, jobStatus.getSourceUid());
+                pw.print(": ");
                 pw.print(jobStatus.getSourcePackageName());
-                pw.print(": runnable=");
+                pw.print(", runnable=");
                 pw.println((jobStatus.satisfiedConstraints
                         & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0);
             }
         });
-        pw.println();
     }
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/job/controllers/IdleController.java b/services/core/java/com/android/server/job/controllers/IdleController.java
index 5899d16..f41e187 100644
--- a/services/core/java/com/android/server/job/controllers/IdleController.java
+++ b/services/core/java/com/android/server/job/controllers/IdleController.java
@@ -26,6 +26,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Slog;
 
 import com.android.server.am.ActivityManagerService;
@@ -191,15 +192,19 @@
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
         pw.print("Idle: ");
         pw.println(mIdleTracker.isIdle() ? "true" : "false");
-        pw.println(mTrackedTasks.size());
+        pw.print("Tracking ");
+        pw.print(mTrackedTasks.size());
+        pw.println(":");
         for (int i = 0; i < mTrackedTasks.size(); i++) {
             final JobStatus js = mTrackedTasks.get(i);
             if (!js.shouldDump(filterUid)) {
                 continue;
             }
-            pw.print("  ");
-            pw.print(String.valueOf(js.getJobId() + "," + js.getUid()));
-            pw.println("..");
+            pw.print("  #");
+            js.printUniqueId(pw);
+            pw.print(" from ");
+            UserHandle.formatUid(pw, js.getSourceUid());
+            pw.println();
         }
     }
 }
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 072787b..ded7a2f 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -249,6 +249,12 @@
         return job.getId();
     }
 
+    public void printUniqueId(PrintWriter pw) {
+        UserHandle.formatUid(pw, callingUid);
+        pw.print("/");
+        pw.print(job.getId());
+    }
+
     public int getNumFailures() {
         return numFailures;
     }
@@ -410,6 +416,10 @@
         return true;
     }
 
+    boolean isConstraintSatisfied(int constraint) {
+        return (satisfiedConstraints&constraint) != 0;
+    }
+
     public boolean shouldDump(int filterUid) {
         return filterUid == -1 || UserHandle.getAppId(getUid()) == filterUid
                 || UserHandle.getAppId(getSourceUid()) == filterUid;
@@ -505,10 +515,22 @@
     public String toShortString() {
         StringBuilder sb = new StringBuilder();
         sb.append(Integer.toHexString(System.identityHashCode(this)));
-        sb.append(" jId=");
+        sb.append(" #");
+        UserHandle.formatUid(sb, callingUid);
+        sb.append("/");
         sb.append(job.getId());
         sb.append(' ');
-        UserHandle.formatUid(sb, callingUid);
+        sb.append(batteryName);
+        return sb.toString();
+    }
+
+    /**
+     * Convenience function to identify a job uniquely without pulling all the data that
+     * {@link #toString()} returns.
+     */
+    public String toShortStringExceptUniqueId() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(Integer.toHexString(System.identityHashCode(this)));
         sb.append(' ');
         sb.append(batteryName);
         return sb.toString();
diff --git a/services/core/java/com/android/server/job/controllers/TimeController.java b/services/core/java/com/android/server/job/controllers/TimeController.java
index 2f8ca7e..0b3b00f 100644
--- a/services/core/java/com/android/server/job/controllers/TimeController.java
+++ b/services/core/java/com/android/server/job/controllers/TimeController.java
@@ -20,7 +20,10 @@
 import android.app.AlarmManager.OnAlarmListener;
 import android.content.Context;
 import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.WorkSource;
 import android.util.Slog;
+import android.util.TimeUtils;
 
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.StateChangedListener;
@@ -39,9 +42,9 @@
     private static final String TAG = "JobScheduler.Time";
 
     /** Deadline alarm tag for logging purposes */
-    private final String DEADLINE_TAG = "JobScheduler.deadline";
+    private final String DEADLINE_TAG = "*job.deadline*";
     /** Delay alarm tag for logging purposes */
-    private final String DELAY_TAG = "JobScheduler.delay";
+    private final String DELAY_TAG = "*job.delay*";
 
     private long mNextJobExpiredElapsedMillis;
     private long mNextDelayExpiredElapsedMillis;
@@ -91,7 +94,8 @@
             it.add(job);
             maybeUpdateAlarmsLocked(
                     job.hasTimingDelayConstraint() ? job.getEarliestRunTime() : Long.MAX_VALUE,
-                    job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE);
+                    job.hasDeadlineConstraint() ? job.getLatestRunTimeElapsed() : Long.MAX_VALUE,
+                    job.getSourceUid());
         }
     }
 
@@ -134,6 +138,7 @@
     private void checkExpiredDeadlinesAndResetAlarm() {
         synchronized (mLock) {
             long nextExpiryTime = Long.MAX_VALUE;
+            int nextExpiryUid = 0;
             final long nowElapsedMillis = SystemClock.elapsedRealtime();
 
             Iterator<JobStatus> it = mTrackedJobs.iterator();
@@ -153,10 +158,11 @@
                     it.remove();
                 } else {  // Sorted by expiry time, so take the next one and stop.
                     nextExpiryTime = jobDeadline;
+                    nextExpiryUid = job.getSourceUid();
                     break;
                 }
             }
-            setDeadlineExpiredAlarmLocked(nextExpiryTime);
+            setDeadlineExpiredAlarmLocked(nextExpiryTime, nextExpiryUid);
         }
     }
 
@@ -168,6 +174,7 @@
         synchronized (mLock) {
             final long nowElapsedMillis = SystemClock.elapsedRealtime();
             long nextDelayTime = Long.MAX_VALUE;
+            int nextDelayUid = 0;
             boolean ready = false;
             Iterator<JobStatus> it = mTrackedJobs.iterator();
             while (it.hasNext()) {
@@ -184,25 +191,29 @@
                     if (job.isReady()) {
                         ready = true;
                     }
-                } else {  // Keep going through list to get next delay time.
+                } else if (!job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)) {
+                    // If this job still doesn't have its delay constraint satisfied,
+                    // then see if it is the next upcoming delay time for the alarm.
                     if (nextDelayTime > jobDelayTime) {
                         nextDelayTime = jobDelayTime;
+                        nextDelayUid = job.getSourceUid();
                     }
                 }
             }
             if (ready) {
                 mStateChangedListener.onControllerStateChanged();
             }
-            setDelayExpiredAlarmLocked(nextDelayTime);
+            setDelayExpiredAlarmLocked(nextDelayTime, nextDelayUid);
         }
     }
 
-    private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed) {
+    private void maybeUpdateAlarmsLocked(long delayExpiredElapsed, long deadlineExpiredElapsed,
+            int uid) {
         if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
-            setDelayExpiredAlarmLocked(delayExpiredElapsed);
+            setDelayExpiredAlarmLocked(delayExpiredElapsed, uid);
         }
         if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
-            setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed);
+            setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, uid);
         }
     }
 
@@ -211,11 +222,11 @@
      * delay will expire.
      * This alarm <b>will</b> wake up the phone.
      */
-    private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis) {
+    private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) {
         alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
         mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
         updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener,
-                mNextDelayExpiredElapsedMillis);
+                mNextDelayExpiredElapsedMillis, uid);
     }
 
     /**
@@ -223,11 +234,11 @@
      * deadline will expire.
      * This alarm <b>will</b> wake up the phone.
      */
-    private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis) {
+    private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, int uid) {
         alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
         mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
         updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener,
-                mNextJobExpiredElapsedMillis);
+                mNextJobExpiredElapsedMillis, uid);
     }
 
     private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
@@ -239,7 +250,7 @@
     }
 
     private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
-            long alarmTimeElapsed) {
+            long alarmTimeElapsed, int uid) {
         ensureAlarmServiceLocked();
         if (alarmTimeElapsed == Long.MAX_VALUE) {
             mAlarmService.cancel(listener);
@@ -248,7 +259,7 @@
                 Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed);
             }
             mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed,
-                    tag, listener, null);
+                    AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, new WorkSource(uid));
         }
     }
 
@@ -277,20 +288,39 @@
     @Override
     public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
         final long nowElapsed = SystemClock.elapsedRealtime();
-        pw.println("Alarms (" + SystemClock.elapsedRealtime() + ")");
-        pw.println(
-                "Next delay alarm in " + (mNextDelayExpiredElapsedMillis - nowElapsed)/1000 + "s");
-        pw.println("Next deadline alarm in " + (mNextJobExpiredElapsedMillis - nowElapsed)/1000
-                + "s");
-        pw.println("Tracking:");
+        pw.print("Alarms: now=");
+        pw.print(SystemClock.elapsedRealtime());
+        pw.println();
+        pw.print("Next delay alarm in ");
+        TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw);
+        pw.println();
+        pw.print("Next deadline alarm in ");
+        TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw);
+        pw.println();
+        pw.print("Tracking ");
+        pw.print(mTrackedJobs.size());
+        pw.println(":");
         for (JobStatus ts : mTrackedJobs) {
             if (!ts.shouldDump(filterUid)) {
                 continue;
             }
-            pw.println(String.valueOf(ts.getJobId() + "," + ts.getUid())
-                    + ": (" + (ts.hasTimingDelayConstraint() ? ts.getEarliestRunTime() : "N/A")
-                    + ", " + (ts.hasDeadlineConstraint() ?ts.getLatestRunTimeElapsed() : "N/A")
-                    + ")");
+            pw.print("  #");
+            ts.printUniqueId(pw);
+            pw.print(" from ");
+            UserHandle.formatUid(pw, ts.getSourceUid());
+            pw.print(": Delay=");
+            if (ts.hasTimingDelayConstraint()) {
+                TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw);
+            } else {
+                pw.print("N/A");
+            }
+            pw.print(", Deadline=");
+            if (ts.hasDeadlineConstraint()) {
+                TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw);
+            } else {
+                pw.print("N/A");
+            }
+            pw.println();
         }
     }
 }
\ No newline at end of file