Merge "Introduce concept of work types." into sc-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 7cad4ab..e05f0b0 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.job;
 
+import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.job.JobInfo;
 import android.content.BroadcastReceiver;
@@ -26,8 +28,11 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.provider.DeviceConfig;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
+import android.util.Pair;
 import android.util.Slog;
+import android.util.SparseIntArray;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 
@@ -36,16 +41,17 @@
 import com.android.internal.app.procstats.ProcessStats;
 import com.android.internal.util.StatLogger;
 import com.android.server.JobSchedulerBackgroundThread;
-import com.android.server.job.JobSchedulerService.MaxJobCountsPerMemoryTrimLevel;
 import com.android.server.job.controllers.JobStatus;
 import com.android.server.job.controllers.StateController;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Iterator;
 import java.util.List;
 
 /**
- * This class decides, given the various configuration and the system status, how many more jobs
- * can start.
+ * This class decides, given the various configuration and the system status, which jobs can start
+ * and which {@link JobServiceContext} to run each job on.
  */
 class JobConcurrencyManager {
     private static final String TAG = JobSchedulerService.TAG;
@@ -56,9 +62,22 @@
             CONFIG_KEY_PREFIX_CONCURRENCY + "screen_off_adjustment_delay_ms";
     private static final long DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS = 30_000;
 
+    static final int WORK_TYPE_NONE = 0;
+    static final int WORK_TYPE_TOP = 1 << 0;
+    static final int WORK_TYPE_BG = 1 << 1;
+    private static final int NUM_WORK_TYPES = 2;
+
+    @IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = {
+            WORK_TYPE_NONE,
+            WORK_TYPE_TOP,
+            WORK_TYPE_BG
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface WorkType {
+    }
+
     private final Object mLock;
     private final JobSchedulerService mService;
-    private final JobSchedulerService.Constants mConstants;
     private final Context mContext;
     private final Handler mHandler;
 
@@ -72,6 +91,53 @@
 
     private static final int MAX_JOB_CONTEXTS_COUNT = JobSchedulerService.MAX_JOB_CONTEXTS_COUNT;
 
+    private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_ON =
+            new WorkConfigLimitsPerMemoryTrimLevel(
+                    new WorkTypeConfig("screen_on_normal", 8,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 2), Pair.create(WORK_TYPE_BG, 2)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 6))),
+                    new WorkTypeConfig("screen_on_moderate", 8,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 2)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 4))),
+                    new WorkTypeConfig("screen_on_low", 5,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 1)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 1))),
+                    new WorkTypeConfig("screen_on_critical", 5,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 1)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 1)))
+            );
+    private static final WorkConfigLimitsPerMemoryTrimLevel CONFIG_LIMITS_SCREEN_OFF =
+            new WorkConfigLimitsPerMemoryTrimLevel(
+                    new WorkTypeConfig("screen_off_normal", 10,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 2)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 6))),
+                    new WorkTypeConfig("screen_off_moderate", 10,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 6), Pair.create(WORK_TYPE_BG, 2)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 4))),
+                    new WorkTypeConfig("screen_off_low", 5,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 1)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 1))),
+                    new WorkTypeConfig("screen_off_critical", 5,
+                            // defaultMin
+                            List.of(Pair.create(WORK_TYPE_TOP, 4), Pair.create(WORK_TYPE_BG, 1)),
+                            // defaultMax
+                            List.of(Pair.create(WORK_TYPE_BG, 1)))
+            );
+
     /**
      * This array essentially stores the state of mActiveServices array.
      * The ith index stores the job present on the ith JobServiceContext.
@@ -84,10 +150,11 @@
 
     int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT];
 
-    /** Max job counts according to the current system state. */
-    private JobSchedulerService.MaxJobCounts mMaxJobCounts;
+    int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT];
 
-    private final JobCountTracker mJobCountTracker = new JobCountTracker();
+    private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>();
+
+    private final WorkCountTracker mWorkCountTracker = new WorkCountTracker();
 
     /** Wait for this long after screen off before adjusting the job concurrency. */
     private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS;
@@ -114,7 +181,6 @@
     JobConcurrencyManager(JobSchedulerService service) {
         mService = service;
         mLock = mService.mLock;
-        mConstants = service.mConstants;
         mContext = service.getContext();
 
         mHandler = JobSchedulerBackgroundThread.getHandler();
@@ -209,12 +275,6 @@
         }
     }
 
-    private boolean isFgJob(JobStatus job) {
-        // (It's super confusing PRIORITY_BOUND_FOREGROUND_SERVICE isn't FG here)
-        return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP
-                || job.shouldTreatAsExpeditedJob();
-    }
-
     @GuardedBy("mLock")
     private void refreshSystemStateLocked() {
         final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis();
@@ -237,28 +297,29 @@
     }
 
     @GuardedBy("mLock")
-    private void updateMaxCountsLocked() {
+    private void updateCounterConfigLocked() {
         refreshSystemStateLocked();
 
-        final MaxJobCountsPerMemoryTrimLevel jobCounts = mEffectiveInteractiveState
-                ? mConstants.MAX_JOB_COUNTS_SCREEN_ON
-                : mConstants.MAX_JOB_COUNTS_SCREEN_OFF;
+        final WorkConfigLimitsPerMemoryTrimLevel workConfigs = mEffectiveInteractiveState
+                ? CONFIG_LIMITS_SCREEN_ON : CONFIG_LIMITS_SCREEN_OFF;
 
-
+        WorkTypeConfig workTypeConfig;
         switch (mLastMemoryTrimLevel) {
             case ProcessStats.ADJ_MEM_FACTOR_MODERATE:
-                mMaxJobCounts = jobCounts.moderate;
+                workTypeConfig = workConfigs.moderate;
                 break;
             case ProcessStats.ADJ_MEM_FACTOR_LOW:
-                mMaxJobCounts = jobCounts.low;
+                workTypeConfig = workConfigs.low;
                 break;
             case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
-                mMaxJobCounts = jobCounts.critical;
+                workTypeConfig = workConfigs.critical;
                 break;
             default:
-                mMaxJobCounts = jobCounts.normal;
+                workTypeConfig = workConfigs.normal;
                 break;
         }
+
+        mWorkCountTracker.setConfig(workTypeConfig);
     }
 
     /**
@@ -282,31 +343,26 @@
             Slog.d(TAG, printPendingQueueLocked());
         }
 
-        final JobPackageTracker tracker = mService.mJobPackageTracker;
         final List<JobStatus> pendingJobs = mService.mPendingJobs;
         final List<JobServiceContext> activeServices = mService.mActiveServices;
-        final List<StateController> controllers = mService.mControllers;
-
-        updateMaxCountsLocked();
 
         // To avoid GC churn, we recycle the arrays.
         JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap;
         boolean[] slotChanged = mRecycledSlotChanged;
         int[] preferredUidForContext = mRecycledPreferredUidForContext;
+        int[] workTypeForContext = mRecycledWorkTypeForContext;
 
+        updateCounterConfigLocked();
+        // Reset everything since we'll re-evaluate the current state.
+        mWorkCountTracker.resetCounts();
 
-        // Initialize the work variables and also count running jobs.
-        mJobCountTracker.reset(
-                mMaxJobCounts.getMaxTotal(),
-                mMaxJobCounts.getMaxBg(),
-                mMaxJobCounts.getMinBg());
-
-        for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
+        for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
             final JobServiceContext js = mService.mActiveServices.get(i);
             final JobStatus status = js.getRunningJobLocked();
 
             if ((contextIdToJobMap[i] = status) != null) {
-                mJobCountTracker.incrementRunningJobCount(isFgJob(status));
+                mWorkCountTracker.incrementRunningJobCount(js.getRunningJobWorkType());
+                workTypeForContext[i] = js.getRunningJobWorkType();
             }
 
             slotChanged[i] = false;
@@ -317,41 +373,25 @@
         }
 
         // Next, update the job priorities, and also count the pending FG / BG jobs.
-        for (int i = 0; i < pendingJobs.size(); i++) {
-            final JobStatus pending = pendingJobs.get(i);
+        updateNonRunningPriorities(pendingJobs, true);
 
-            // If job is already running, go to next job.
-            int jobRunningContext = findJobContextIdFromMap(pending, contextIdToJobMap);
-            if (jobRunningContext != -1) {
-                continue;
-            }
-
-            final int priority = mService.evaluateJobPriorityLocked(pending);
-            pending.lastEvaluatedPriority = priority;
-
-            mJobCountTracker.incrementPendingJobCount(isFgJob(pending));
-        }
-
-        mJobCountTracker.onCountDone();
+        mWorkCountTracker.onCountDone();
 
         for (int i = 0; i < pendingJobs.size(); i++) {
             final JobStatus nextPending = pendingJobs.get(i);
 
-            // Unfortunately we need to repeat this relatively expensive check.
-            int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap);
-            if (jobRunningContext != -1) {
+            if (mRunningJobs.contains(nextPending)) {
                 continue;
             }
 
             // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them
 
-            final boolean isPendingFg = isFgJob(nextPending);
-
             // Find an available slot for nextPending. The context should be available OR
             // it should have lowest priority among all running jobs
             // (sharing the same Uid as nextPending)
             int minPriorityForPreemption = Integer.MAX_VALUE;
             int selectedContextId = -1;
+            int workType = mWorkCountTracker.canJobStart(getJobWorkTypes(nextPending));
             boolean startingJob = false;
             for (int j=0; j<MAX_JOB_CONTEXTS_COUNT; j++) {
                 JobStatus job = contextIdToJobMap[j];
@@ -360,7 +400,7 @@
                     final boolean preferredUidOkay = (preferredUid == nextPending.getUid())
                             || (preferredUid == JobServiceContext.NO_PREFERRED_UID);
 
-                    if (preferredUidOkay && mJobCountTracker.canJobStart(isPendingFg)) {
+                    if (preferredUidOkay && workType != WORK_TYPE_NONE) {
                         // This slot is free, and we haven't yet hit the limit on
                         // concurrent jobs...  we can just throw the job in to here.
                         selectedContextId = j;
@@ -396,19 +436,19 @@
             }
             if (startingJob) {
                 // Increase the counters when we're going to start a job.
-                mJobCountTracker.onStartingNewJob(isPendingFg);
+                workTypeForContext[selectedContextId] = workType;
+                mWorkCountTracker.stageJob(workType);
             }
         }
         if (DEBUG) {
             Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final"));
         }
 
-        mJobCountTracker.logStatus();
+        if (DEBUG) {
+            Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString());
+        }
 
-        tracker.noteConcurrency(mJobCountTracker.getTotalRunningJobCountToNote(),
-                mJobCountTracker.getFgRunningJobCountToNote());
-
-        for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
+        for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
             boolean preservePreferredUid = false;
             if (slotChanged[i]) {
                 JobStatus js = activeServices.get(i).getRunningJobLocked();
@@ -426,30 +466,58 @@
                         Slog.d(TAG, "About to run job on context "
                                 + i + ", job: " + pendingJob);
                     }
-                    for (int ic=0; ic<controllers.size(); ic++) {
-                        controllers.get(ic).prepareForExecutionLocked(pendingJob);
-                    }
-                    if (!activeServices.get(i).executeRunnableJob(pendingJob)) {
-                        Slog.d(TAG, "Error executing " + pendingJob);
-                    }
-                    if (pendingJobs.remove(pendingJob)) {
-                        tracker.noteNonpending(pendingJob);
-                    }
+                    startJobLocked(activeServices.get(i), pendingJob, workTypeForContext[i]);
                 }
             }
             if (!preservePreferredUid) {
                 activeServices.get(i).clearPreferredUid();
             }
         }
+        mWorkCountTracker.resetStagingCount();
+        noteConcurrency();
     }
 
-    private static int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) {
-        for (int i=0; i<map.length; i++) {
-            if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) {
-                return i;
+    private void noteConcurrency() {
+        mService.mJobPackageTracker.noteConcurrency(mRunningJobs.size(),
+                // TODO: log per type instead of only TOP
+                mWorkCountTracker.getRunningJobCount(WORK_TYPE_TOP));
+    }
+
+    private void updateNonRunningPriorities(@NonNull final List<JobStatus> pendingJobs,
+            boolean updateCounter) {
+        for (int i = 0; i < pendingJobs.size(); i++) {
+            final JobStatus pending = pendingJobs.get(i);
+
+            // If job is already running, go to next job.
+            if (mRunningJobs.contains(pending)) {
+                continue;
+            }
+
+            pending.lastEvaluatedPriority = mService.evaluateJobPriorityLocked(pending);
+
+            if (updateCounter) {
+                mWorkCountTracker.incrementPendingJobCount(getJobWorkTypes(pending));
             }
         }
-        return -1;
+    }
+
+    private void startJobLocked(@NonNull JobServiceContext worker, @NonNull JobStatus jobStatus,
+            @WorkType final int workType) {
+        final List<StateController> controllers = mService.mControllers;
+        for (int ic = 0; ic < controllers.size(); ic++) {
+            controllers.get(ic).prepareForExecutionLocked(jobStatus);
+        }
+        if (!worker.executeRunnableJob(jobStatus, workType)) {
+            Slog.e(TAG, "Error executing " + jobStatus);
+            mWorkCountTracker.onStagedJobFailed(workType);
+        } else {
+            mRunningJobs.add(jobStatus);
+            mWorkCountTracker.onJobStarted(workType);
+        }
+        final List<JobStatus> pendingJobs = mService.mPendingJobs;
+        if (pendingJobs.remove(jobStatus)) {
+            mService.mJobPackageTracker.noteNonpending(jobStatus);
+        }
     }
 
     @GuardedBy("mLock")
@@ -484,6 +552,16 @@
 
         mScreenOffAdjustmentDelayMs = properties.getLong(
                 KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS);
+
+        CONFIG_LIMITS_SCREEN_ON.normal.update(properties);
+        CONFIG_LIMITS_SCREEN_ON.moderate.update(properties);
+        CONFIG_LIMITS_SCREEN_ON.low.update(properties);
+        CONFIG_LIMITS_SCREEN_ON.critical.update(properties);
+
+        CONFIG_LIMITS_SCREEN_OFF.normal.update(properties);
+        CONFIG_LIMITS_SCREEN_OFF.moderate.update(properties);
+        CONFIG_LIMITS_SCREEN_OFF.low.update(properties);
+        CONFIG_LIMITS_SCREEN_OFF.critical.update(properties);
     }
 
     public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) {
@@ -494,6 +572,14 @@
             pw.print("Configuration:");
             pw.increaseIndent();
             pw.print(KEY_SCREEN_OFF_ADJUSTMENT_DELAY_MS, mScreenOffAdjustmentDelayMs).println();
+            CONFIG_LIMITS_SCREEN_ON.normal.dump(pw);
+            CONFIG_LIMITS_SCREEN_ON.moderate.dump(pw);
+            CONFIG_LIMITS_SCREEN_ON.low.dump(pw);
+            CONFIG_LIMITS_SCREEN_ON.critical.dump(pw);
+            CONFIG_LIMITS_SCREEN_OFF.normal.dump(pw);
+            CONFIG_LIMITS_SCREEN_OFF.moderate.dump(pw);
+            CONFIG_LIMITS_SCREEN_OFF.low.dump(pw);
+            CONFIG_LIMITS_SCREEN_OFF.critical.dump(pw);
             pw.decreaseIndent();
 
             pw.print("Screen state: current ");
@@ -514,7 +600,7 @@
 
             pw.println("Current max jobs:");
             pw.println("  ");
-            pw.println(mJobCountTracker);
+            pw.println(mWorkCountTracker);
 
             pw.println();
 
@@ -540,8 +626,6 @@
         proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS,
                 nowRealtime - mLastScreenOffRealtime);
 
-        mJobCountTracker.dumpProto(proto, JobConcurrencyManagerProto.JOB_COUNT_TRACKER);
-
         proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL, mLastMemoryTrimLevel);
 
         mStatLogger.dumpProto(proto, JobConcurrencyManagerProto.STATS);
@@ -549,199 +633,312 @@
         proto.end(token);
     }
 
+    int getJobWorkTypes(@NonNull JobStatus js) {
+        int classification = 0;
+        // TODO(171305774): create dedicated work type for EJ and FGS
+        if (js.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP
+                || js.shouldTreatAsExpeditedJob()) {
+            classification |= WORK_TYPE_TOP;
+        } else {
+            classification |= WORK_TYPE_BG;
+        }
+        return classification;
+    }
+
+    @VisibleForTesting
+    static class WorkTypeConfig {
+        private static final String KEY_PREFIX_MAX_TOTAL =
+                CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_";
+        private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_";
+        private static final String KEY_PREFIX_MAX_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "max_bg_";
+        private static final String KEY_PREFIX_MIN_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "min_top_";
+        private static final String KEY_PREFIX_MIN_BG = CONFIG_KEY_PREFIX_CONCURRENCY + "min_bg_";
+        private final String mConfigIdentifier;
+
+        private int mMaxTotal;
+        private final SparseIntArray mMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES);
+        private final SparseIntArray mMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES);
+        private final int mDefaultMaxTotal;
+        private final SparseIntArray mDefaultMinReservedSlots = new SparseIntArray(NUM_WORK_TYPES);
+        private final SparseIntArray mDefaultMaxAllowedSlots = new SparseIntArray(NUM_WORK_TYPES);
+
+        WorkTypeConfig(@NonNull String configIdentifier, int defaultMaxTotal,
+                List<Pair<Integer, Integer>> defaultMin, List<Pair<Integer, Integer>> defaultMax) {
+            mConfigIdentifier = configIdentifier;
+            mDefaultMaxTotal = mMaxTotal = defaultMaxTotal;
+            for (int i = defaultMin.size() - 1; i >= 0; --i) {
+                mDefaultMinReservedSlots.put(defaultMin.get(i).first, defaultMin.get(i).second);
+            }
+            for (int i = defaultMax.size() - 1; i >= 0; --i) {
+                mDefaultMaxAllowedSlots.put(defaultMax.get(i).first, defaultMax.get(i).second);
+            }
+            update(new DeviceConfig.Properties.Builder(
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER).build());
+        }
+
+        void update(@NonNull DeviceConfig.Properties properties) {
+            // Ensure total in the range [1, MAX_JOB_CONTEXTS_COUNT].
+            mMaxTotal = Math.max(1, Math.min(MAX_JOB_CONTEXTS_COUNT,
+                    properties.getInt(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mDefaultMaxTotal)));
+
+            mMaxAllowedSlots.clear();
+            // Ensure they're in the range [1, total].
+            final int maxTop = Math.max(1, Math.min(mMaxTotal,
+                    properties.getInt(KEY_PREFIX_MAX_TOP + mConfigIdentifier,
+                            mDefaultMaxAllowedSlots.get(WORK_TYPE_TOP, mMaxTotal))));
+            mMaxAllowedSlots.put(WORK_TYPE_TOP, maxTop);
+            final int maxBg = Math.max(1, Math.min(mMaxTotal,
+                    properties.getInt(KEY_PREFIX_MAX_BG + mConfigIdentifier,
+                            mDefaultMaxAllowedSlots.get(WORK_TYPE_BG, mMaxTotal))));
+            mMaxAllowedSlots.put(WORK_TYPE_BG, maxBg);
+
+            int remaining = mMaxTotal;
+            mMinReservedSlots.clear();
+            // Ensure top is in the range [1, min(maxTop, total)]
+            final int minTop = Math.max(1, Math.min(Math.min(maxTop, mMaxTotal),
+                    properties.getInt(KEY_PREFIX_MIN_TOP + mConfigIdentifier,
+                            mDefaultMinReservedSlots.get(WORK_TYPE_TOP))));
+            mMinReservedSlots.put(WORK_TYPE_TOP, minTop);
+            remaining -= minTop;
+            // Ensure bg is in the range [0, min(maxBg, remaining)]
+            final int minBg = Math.max(0, Math.min(Math.min(maxBg, remaining),
+                    properties.getInt(KEY_PREFIX_MIN_BG + mConfigIdentifier,
+                            mDefaultMinReservedSlots.get(WORK_TYPE_BG))));
+            mMinReservedSlots.put(WORK_TYPE_BG, minBg);
+        }
+
+        int getMaxTotal() {
+            return mMaxTotal;
+        }
+
+        int getMax(@WorkType int workType) {
+            return mMaxAllowedSlots.get(workType, mMaxTotal);
+        }
+
+        int getMinReserved(@WorkType int workType) {
+            return mMinReservedSlots.get(workType);
+        }
+
+        void dump(IndentingPrintWriter pw) {
+            pw.print(KEY_PREFIX_MAX_TOTAL + mConfigIdentifier, mMaxTotal).println();
+            pw.print(KEY_PREFIX_MIN_TOP + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_TOP))
+                    .println();
+            pw.print(KEY_PREFIX_MAX_TOP + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_TOP))
+                    .println();
+            pw.print(KEY_PREFIX_MIN_BG + mConfigIdentifier, mMinReservedSlots.get(WORK_TYPE_BG))
+                    .println();
+            pw.print(KEY_PREFIX_MAX_BG + mConfigIdentifier, mMaxAllowedSlots.get(WORK_TYPE_BG))
+                    .println();
+        }
+    }
+
+    /** {@link WorkTypeConfig} for each memory trim level. */
+    static class WorkConfigLimitsPerMemoryTrimLevel {
+        public final WorkTypeConfig normal;
+        public final WorkTypeConfig moderate;
+        public final WorkTypeConfig low;
+        public final WorkTypeConfig critical;
+
+        WorkConfigLimitsPerMemoryTrimLevel(WorkTypeConfig normal, WorkTypeConfig moderate,
+                WorkTypeConfig low, WorkTypeConfig critical) {
+            this.normal = normal;
+            this.moderate = moderate;
+            this.low = low;
+            this.critical = critical;
+        }
+    }
+
     /**
-     * This class decides, taking into account {@link #mMaxJobCounts} and how mny jos are running /
-     * pending, how many more job can start.
+     * This class decides, taking into account the current {@link WorkTypeConfig} and how many jobs
+     * are running/pending, how many more job can start.
      *
      * Extracted for testing and logging.
      */
     @VisibleForTesting
-    static class JobCountTracker {
-        private int mConfigNumMaxTotalJobs;
-        private int mConfigNumMaxBgJobs;
-        private int mConfigNumMinBgJobs;
+    static class WorkCountTracker {
+        private int mConfigMaxTotal;
+        private final SparseIntArray mConfigNumReservedSlots = new SparseIntArray(NUM_WORK_TYPES);
+        private final SparseIntArray mConfigAbsoluteMaxSlots = new SparseIntArray(NUM_WORK_TYPES);
 
-        private int mNumRunningFgJobs;
-        private int mNumRunningBgJobs;
+        /**
+         * Numbers may be lower in this than in {@link #mConfigNumReservedSlots} if there aren't
+         * enough ready jobs of a type to take up all of the desired reserved slots.
+         */
+        private final SparseIntArray mNumActuallyReservedSlots = new SparseIntArray(NUM_WORK_TYPES);
+        private final SparseIntArray mNumPendingJobs = new SparseIntArray(NUM_WORK_TYPES);
+        private final SparseIntArray mNumRunningJobs = new SparseIntArray(NUM_WORK_TYPES);
+        private final SparseIntArray mNumStartingJobs = new SparseIntArray(NUM_WORK_TYPES);
+        private int mNumUnspecialized = 0;
+        private int mNumUnspecializedRemaining = 0;
 
-        private int mNumPendingFgJobs;
-        private int mNumPendingBgJobs;
+        void setConfig(@NonNull WorkTypeConfig workTypeConfig) {
+            mConfigMaxTotal = workTypeConfig.getMaxTotal();
+            mConfigNumReservedSlots.put(WORK_TYPE_TOP,
+                    workTypeConfig.getMinReserved(WORK_TYPE_TOP));
+            mConfigNumReservedSlots.put(WORK_TYPE_BG, workTypeConfig.getMinReserved(WORK_TYPE_BG));
+            mConfigAbsoluteMaxSlots.put(WORK_TYPE_TOP, workTypeConfig.getMax(WORK_TYPE_TOP));
+            mConfigAbsoluteMaxSlots.put(WORK_TYPE_BG, workTypeConfig.getMax(WORK_TYPE_BG));
 
-        private int mNumStartingFgJobs;
-        private int mNumStartingBgJobs;
-
-        private int mNumReservedForBg;
-        private int mNumActualMaxFgJobs;
-        private int mNumActualMaxBgJobs;
-
-        void reset(int numTotalMaxJobs, int numMaxBgJobs, int numMinBgJobs) {
-            mConfigNumMaxTotalJobs = numTotalMaxJobs;
-            mConfigNumMaxBgJobs = numMaxBgJobs;
-            mConfigNumMinBgJobs = numMinBgJobs;
-
-            mNumRunningFgJobs = 0;
-            mNumRunningBgJobs = 0;
-
-            mNumPendingFgJobs = 0;
-            mNumPendingBgJobs = 0;
-
-            mNumStartingFgJobs = 0;
-            mNumStartingBgJobs = 0;
-
-            mNumReservedForBg = 0;
-            mNumActualMaxFgJobs = 0;
-            mNumActualMaxBgJobs = 0;
+            mNumUnspecialized = mConfigMaxTotal;
+            mNumUnspecialized -= mConfigNumReservedSlots.get(WORK_TYPE_TOP);
+            mNumUnspecialized -= mConfigNumReservedSlots.get(WORK_TYPE_BG);
+            mNumUnspecialized -= mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP);
+            mNumUnspecialized -= mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG);
+            calculateUnspecializedRemaining();
         }
 
-        void incrementRunningJobCount(boolean isFg) {
-            if (isFg) {
-                mNumRunningFgJobs++;
-            } else {
-                mNumRunningBgJobs++;
+        private void calculateUnspecializedRemaining() {
+            mNumUnspecializedRemaining = mNumUnspecialized;
+            for (int i = mNumRunningJobs.size() - 1; i >= 0; --i) {
+                mNumUnspecializedRemaining -= mNumRunningJobs.valueAt(i);
             }
         }
 
-        void incrementPendingJobCount(boolean isFg) {
-            if (isFg) {
-                mNumPendingFgJobs++;
-            } else {
-                mNumPendingBgJobs++;
+        void resetCounts() {
+            mNumActuallyReservedSlots.clear();
+            mNumPendingJobs.clear();
+            mNumRunningJobs.clear();
+            resetStagingCount();
+        }
+
+        void resetStagingCount() {
+            mNumStartingJobs.clear();
+        }
+
+        void incrementRunningJobCount(@WorkType int workType) {
+            mNumRunningJobs.put(workType, mNumRunningJobs.get(workType) + 1);
+        }
+
+        void incrementPendingJobCount(int workTypes) {
+            // We don't know which type we'll classify the job as when we run it yet, so make sure
+            // we have space in all applicable slots.
+            if ((workTypes & WORK_TYPE_TOP) == WORK_TYPE_TOP) {
+                mNumPendingJobs.put(WORK_TYPE_TOP, mNumPendingJobs.get(WORK_TYPE_TOP) + 1);
+            }
+            if ((workTypes & WORK_TYPE_BG) == WORK_TYPE_BG) {
+                mNumPendingJobs.put(WORK_TYPE_BG, mNumPendingJobs.get(WORK_TYPE_BG) + 1);
             }
         }
 
-        void onStartingNewJob(boolean isFg) {
-            if (isFg) {
-                mNumStartingFgJobs++;
+        void stageJob(@WorkType int workType) {
+            final int newNumStartingJobs = mNumStartingJobs.get(workType) + 1;
+            mNumStartingJobs.put(workType, newNumStartingJobs);
+            mNumPendingJobs.put(workType, Math.max(0, mNumPendingJobs.get(workType) - 1));
+            if (newNumStartingJobs + mNumRunningJobs.get(workType)
+                    > mNumActuallyReservedSlots.get(workType)) {
+                mNumUnspecializedRemaining--;
+            }
+        }
+
+        void onStagedJobFailed(@WorkType int workType) {
+            final int oldNumStartingJobs = mNumStartingJobs.get(workType);
+            if (oldNumStartingJobs == 0) {
+                Slog.e(TAG, "# staged jobs for " + workType + " went negative.");
+                // We are in a bad state. We will eventually recover when the pending list is
+                // regenerated.
+                return;
+            }
+            mNumStartingJobs.put(workType, oldNumStartingJobs - 1);
+            maybeAdjustReservations(workType);
+        }
+
+        private void maybeAdjustReservations(@WorkType int workType) {
+            // Always make sure we reserve the minimum number of slots in case new jobs become ready
+            // soon.
+            final int numRemainingForType = Math.max(mConfigNumReservedSlots.get(workType),
+                    mNumRunningJobs.get(workType) + mNumStartingJobs.get(workType)
+                            + mNumPendingJobs.get(workType));
+            if (numRemainingForType < mNumActuallyReservedSlots.get(workType)) {
+                // We've run all jobs for this type. Let another type use it now.
+                mNumActuallyReservedSlots.put(workType, numRemainingForType);
+                mNumUnspecializedRemaining++;
+            }
+        }
+
+        void onJobStarted(@WorkType int workType) {
+            mNumRunningJobs.put(workType, mNumRunningJobs.get(workType) + 1);
+            final int oldNumStartingJobs = mNumStartingJobs.get(workType);
+            if (oldNumStartingJobs == 0) {
+                Slog.e(TAG, "# stated jobs for " + workType + " went negative.");
+                // We are in a bad state. We will eventually recover when the pending list is
+                // regenerated. For now, only modify the running count.
             } else {
-                mNumStartingBgJobs++;
+                mNumStartingJobs.put(workType, oldNumStartingJobs - 1);
             }
         }
 
         void onCountDone() {
-            // Note some variables are used only here but are made class members in order to have
-            // them on logcat / dumpsys.
+            // Calculate how many slots to reserve for each work type. "Unspecialized" slots will
+            // be reserved for higher importance types first (ie. top before bg).
+            mNumUnspecialized = mConfigMaxTotal;
+            final int numTop = mNumRunningJobs.get(WORK_TYPE_TOP)
+                    + mNumPendingJobs.get(WORK_TYPE_TOP);
+            final int resTop = Math.min(mConfigNumReservedSlots.get(WORK_TYPE_TOP), numTop);
+            mNumActuallyReservedSlots.put(WORK_TYPE_TOP, resTop);
+            mNumUnspecialized -= resTop;
+            final int numBg = mNumRunningJobs.get(WORK_TYPE_BG) + mNumPendingJobs.get(WORK_TYPE_BG);
+            final int resBg = Math.min(mConfigNumReservedSlots.get(WORK_TYPE_BG), numBg);
+            mNumActuallyReservedSlots.put(WORK_TYPE_BG, resBg);
+            mNumUnspecialized -= resBg;
+            calculateUnspecializedRemaining();
 
-            // How many slots should we allocate to BG jobs at least?
-            // That's basically "getMinBg()", but if there are less jobs, decrease it.
-            // (e.g. even if min-bg is 2, if there's only 1 running+pending job, this has to be 1.)
-            final int reservedForBg = Math.min(
-                    mConfigNumMinBgJobs,
-                    mNumRunningBgJobs + mNumPendingBgJobs);
-
-            // However, if there are FG jobs already running, we have to adjust it.
-            mNumReservedForBg = Math.min(reservedForBg,
-                    mConfigNumMaxTotalJobs - mNumRunningFgJobs);
-
-            // Max FG is [total - [number needed for BG jobs]]
-            // [number needed for BG jobs] is the bigger one of [running BG] or [reserved BG]
-            final int maxFg =
-                    mConfigNumMaxTotalJobs - Math.max(mNumRunningBgJobs, mNumReservedForBg);
-
-            // The above maxFg is the theoretical max. If there are less FG jobs, the actual
-            // max FG will be lower accordingly.
-            mNumActualMaxFgJobs = Math.min(
-                    maxFg,
-                    mNumRunningFgJobs + mNumPendingFgJobs);
-
-            // Max BG is [total - actual max FG], but cap at [config max BG].
-            final int maxBg = Math.min(
-                    mConfigNumMaxBgJobs,
-                    mConfigNumMaxTotalJobs - mNumActualMaxFgJobs);
-
-            // If there are less BG jobs than maxBg, then reduce the actual max BG accordingly.
-            // This isn't needed for the logic to work, but this will give consistent output
-            // on logcat and dumpsys.
-            mNumActualMaxBgJobs = Math.min(
-                    maxBg,
-                    mNumRunningBgJobs + mNumPendingBgJobs);
+            // Assign remaining unspecialized based on ranking.
+            int unspecializedAssigned = Math.max(0,
+                    Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP),
+                            Math.min(mNumUnspecializedRemaining, numTop - resTop)));
+            mNumActuallyReservedSlots.put(WORK_TYPE_TOP, resTop + unspecializedAssigned);
+            mNumUnspecializedRemaining -= unspecializedAssigned;
+            unspecializedAssigned = Math.max(0,
+                    Math.min(mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG),
+                            Math.min(mNumUnspecializedRemaining, numBg - resBg)));
+            mNumActuallyReservedSlots.put(WORK_TYPE_BG, resBg + unspecializedAssigned);
+            mNumUnspecializedRemaining -= unspecializedAssigned;
         }
 
-        boolean canJobStart(boolean isFg) {
-            if (isFg) {
-                return mNumRunningFgJobs + mNumStartingFgJobs < mNumActualMaxFgJobs;
-            } else {
-                return mNumRunningBgJobs + mNumStartingBgJobs < mNumActualMaxBgJobs;
+        int canJobStart(int workTypes) {
+            if ((workTypes & WORK_TYPE_TOP) == WORK_TYPE_TOP) {
+                final int maxAllowed = Math.min(
+                        mConfigAbsoluteMaxSlots.get(WORK_TYPE_TOP),
+                        mNumActuallyReservedSlots.get(WORK_TYPE_TOP) + mNumUnspecializedRemaining);
+                if (mNumRunningJobs.get(WORK_TYPE_TOP) + mNumStartingJobs.get(WORK_TYPE_TOP)
+                        < maxAllowed) {
+                    return WORK_TYPE_TOP;
+                }
             }
-        }
-
-        public int getNumStartingFgJobs() {
-            return mNumStartingFgJobs;
-        }
-
-        public int getNumStartingBgJobs() {
-            return mNumStartingBgJobs;
-        }
-
-        int getTotalRunningJobCountToNote() {
-            return mNumRunningFgJobs + mNumRunningBgJobs
-                    + mNumStartingFgJobs + mNumStartingBgJobs;
-        }
-
-        int getFgRunningJobCountToNote() {
-            return mNumRunningFgJobs + mNumStartingFgJobs;
-        }
-
-        void logStatus() {
-            if (DEBUG) {
-                Slog.d(TAG, "assignJobsToContexts: " + this);
+            if ((workTypes & WORK_TYPE_BG) == WORK_TYPE_BG) {
+                final int maxAllowed = Math.min(
+                        mConfigAbsoluteMaxSlots.get(WORK_TYPE_BG),
+                        mNumActuallyReservedSlots.get(WORK_TYPE_BG) + mNumUnspecializedRemaining);
+                if (mNumRunningJobs.get(WORK_TYPE_BG) + mNumStartingJobs.get(WORK_TYPE_BG)
+                        < maxAllowed) {
+                    return WORK_TYPE_BG;
+                }
             }
+            return WORK_TYPE_NONE;
+        }
+
+        int getRunningJobCount(@WorkType final int workType) {
+            return mNumRunningJobs.get(workType, 0);
         }
 
         public String toString() {
-            final int totalFg = mNumRunningFgJobs + mNumStartingFgJobs;
-            final int totalBg = mNumRunningBgJobs + mNumStartingBgJobs;
-            return String.format(
-                    "Config={tot=%d bg min/max=%d/%d}"
-                            + " Running[FG/BG (total)]: %d / %d (%d)"
-                            + " Pending: %d / %d (%d)"
-                            + " Actual max: %d%s / %d%s (%d%s)"
-                            + " Res BG: %d"
-                            + " Starting: %d / %d (%d)"
-                            + " Total: %d%s / %d%s (%d%s)",
-                    mConfigNumMaxTotalJobs, mConfigNumMinBgJobs, mConfigNumMaxBgJobs,
+            StringBuilder sb = new StringBuilder();
 
-                    mNumRunningFgJobs, mNumRunningBgJobs, mNumRunningFgJobs + mNumRunningBgJobs,
+            sb.append("Config={");
+            sb.append("tot=").append(mConfigMaxTotal);
+            sb.append(" mins=");
+            sb.append(mConfigNumReservedSlots);
+            sb.append(" maxs=");
+            sb.append(mConfigAbsoluteMaxSlots);
+            sb.append("}");
 
-                    mNumPendingFgJobs, mNumPendingBgJobs, mNumPendingFgJobs + mNumPendingBgJobs,
+            sb.append(", act res=").append(mNumActuallyReservedSlots);
+            sb.append(", Pending=").append(mNumPendingJobs);
+            sb.append(", Running=").append(mNumRunningJobs);
+            sb.append(", Staged=").append(mNumStartingJobs);
+            sb.append(", # unspecialized remaining=").append(mNumUnspecializedRemaining);
 
-                    mNumActualMaxFgJobs, (totalFg <= mConfigNumMaxTotalJobs) ? "" : "*",
-                    mNumActualMaxBgJobs, (totalBg <= mConfigNumMaxBgJobs) ? "" : "*",
-                    mNumActualMaxFgJobs + mNumActualMaxBgJobs,
-                    (mNumActualMaxFgJobs + mNumActualMaxBgJobs <= mConfigNumMaxTotalJobs)
-                            ? "" : "*",
-
-                    mNumReservedForBg,
-
-                    mNumStartingFgJobs, mNumStartingBgJobs, mNumStartingFgJobs + mNumStartingBgJobs,
-
-                    totalFg, (totalFg <= mNumActualMaxFgJobs) ? "" : "*",
-                    totalBg, (totalBg <= mNumActualMaxBgJobs) ? "" : "*",
-                    totalFg + totalBg, (totalFg + totalBg <= mConfigNumMaxTotalJobs) ? "" : "*"
-            );
-        }
-
-        public void dumpProto(ProtoOutputStream proto, long fieldId) {
-            final long token = proto.start(fieldId);
-
-            proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_TOTAL_JOBS, mConfigNumMaxTotalJobs);
-            proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_BG_JOBS, mConfigNumMaxBgJobs);
-            proto.write(JobCountTrackerProto.CONFIG_NUM_MIN_BG_JOBS, mConfigNumMinBgJobs);
-
-            proto.write(JobCountTrackerProto.NUM_RUNNING_FG_JOBS, mNumRunningFgJobs);
-            proto.write(JobCountTrackerProto.NUM_RUNNING_BG_JOBS, mNumRunningBgJobs);
-
-            proto.write(JobCountTrackerProto.NUM_PENDING_FG_JOBS, mNumPendingFgJobs);
-            proto.write(JobCountTrackerProto.NUM_PENDING_BG_JOBS, mNumPendingBgJobs);
-
-            proto.write(JobCountTrackerProto.NUM_ACTUAL_MAX_FG_JOBS, mNumActualMaxFgJobs);
-            proto.write(JobCountTrackerProto.NUM_ACTUAL_MAX_BG_JOBS, mNumActualMaxBgJobs);
-
-            proto.write(JobCountTrackerProto.NUM_RESERVED_FOR_BG, mNumReservedForBg);
-
-            proto.write(JobCountTrackerProto.NUM_STARTING_FG_JOBS, mNumStartingFgJobs);
-            proto.write(JobCountTrackerProto.NUM_STARTING_BG_JOBS, mNumStartingBgJobs);
-
-            proto.end(token);
+            return sb.toString();
         }
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 885662c..ba78bda 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -380,7 +380,6 @@
                         default:
                             if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY)
                                     && !concurrencyUpdated) {
-                                mConstants.updateConcurrencyConstantsLocked();
                                 mConcurrencyManager.updateConfigLocked();
                                 concurrencyUpdated = true;
                             } else {
@@ -408,119 +407,6 @@
                 mConstants.API_QUOTA_SCHEDULE_WINDOW_MS);
     }
 
-    static class MaxJobCounts {
-        private final int mTotalDefault;
-        private final String mTotalKey;
-        private final int mMaxBgDefault;
-        private final String mMaxBgKey;
-        private final int mMinBgDefault;
-        private final String mMinBgKey;
-        private int mTotal;
-        private int mMaxBg;
-        private int mMinBg;
-
-        MaxJobCounts(int totalDefault, String totalKey,
-                int maxBgDefault, String maxBgKey, int minBgDefault, String minBgKey) {
-            mTotalKey = totalKey;
-            mTotal = mTotalDefault = totalDefault;
-            mMaxBgKey = maxBgKey;
-            mMaxBg = mMaxBgDefault = maxBgDefault;
-            mMinBgKey = minBgKey;
-            mMinBg = mMinBgDefault = minBgDefault;
-        }
-
-        public void update() {
-            mTotal = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
-                    mTotalKey, mTotalDefault);
-            mMaxBg = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
-                    mMaxBgKey, mMaxBgDefault);
-            mMinBg = DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
-                    mMinBgKey, mMinBgDefault);
-
-            // Ensure total in the range [1, MAX_JOB_CONTEXTS_COUNT].
-            mTotal = Math.min(Math.max(1, mTotal), MAX_JOB_CONTEXTS_COUNT);
-
-            // Ensure maxBg in the range [1, total].
-            mMaxBg = Math.min(Math.max(1, mMaxBg), mTotal);
-
-            // Ensure minBg in the range [0, min(maxBg, total - 1)]
-            mMinBg = Math.min(Math.max(0, mMinBg), Math.min(mMaxBg, mTotal - 1));
-        }
-
-        /** Total number of jobs to run simultaneously. */
-        public int getMaxTotal() {
-            return mTotal;
-        }
-
-        /** Max number of BG (== owned by non-TOP apps) jobs to run simultaneously. */
-        public int getMaxBg() {
-            return mMaxBg;
-        }
-
-        /**
-         * We try to run at least this many BG (== owned by non-TOP apps) jobs, when there are any
-         * pending, rather than always running the TOTAL number of FG jobs.
-         */
-        public int getMinBg() {
-            return mMinBg;
-        }
-
-        public void dump(PrintWriter pw, String prefix) {
-            pw.print(prefix);
-            pw.print(mTotalKey);
-            pw.print("=");
-            pw.print(mTotal);
-            pw.println();
-
-            pw.print(prefix);
-            pw.print(mMaxBgKey);
-            pw.print("=");
-            pw.print(mMaxBg);
-            pw.println();
-
-            pw.print(prefix);
-            pw.print(mMinBgKey);
-            pw.print("=");
-            pw.print(mMinBg);
-            pw.println();
-        }
-
-        public void dumpProto(ProtoOutputStream proto, long fieldId) {
-            final long token = proto.start(fieldId);
-            proto.write(MaxJobCountsProto.TOTAL_JOBS, mTotal);
-            proto.write(MaxJobCountsProto.MAX_BG, mMaxBg);
-            proto.write(MaxJobCountsProto.MIN_BG, mMinBg);
-            proto.end(token);
-        }
-    }
-
-    /** {@link MaxJobCounts} for each memory trim level. */
-    static class MaxJobCountsPerMemoryTrimLevel {
-        public final MaxJobCounts normal;
-        public final MaxJobCounts moderate;
-        public final MaxJobCounts low;
-        public final MaxJobCounts critical;
-
-        MaxJobCountsPerMemoryTrimLevel(
-                MaxJobCounts normal,
-                MaxJobCounts moderate, MaxJobCounts low,
-                MaxJobCounts critical) {
-            this.normal = normal;
-            this.moderate = moderate;
-            this.low = low;
-            this.critical = critical;
-        }
-
-        public void dumpProto(ProtoOutputStream proto, long fieldId) {
-            final long token = proto.start(fieldId);
-            normal.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.NORMAL);
-            moderate.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.MODERATE);
-            low.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.LOW);
-            critical.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.CRITICAL);
-            proto.end(token);
-        }
-    }
-
     /**
      * All times are in milliseconds. Any access to this class or its fields should be done while
      * holding the JobSchedulerService.mLock lock.
@@ -580,49 +466,6 @@
          */
         float MODERATE_USE_FACTOR = DEFAULT_MODERATE_USE_FACTOR;
 
-        /** Prefix for all of the max_job constants. */
-        private static final String KEY_PREFIX_MAX_JOB =
-                JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY + "max_job_";
-
-        // Max job counts for screen on / off, for each memory trim level.
-        final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_ON =
-                new MaxJobCountsPerMemoryTrimLevel(
-                        new MaxJobCounts(
-                                8, KEY_PREFIX_MAX_JOB + "total_on_normal",
-                                6, KEY_PREFIX_MAX_JOB + "max_bg_on_normal",
-                                2, KEY_PREFIX_MAX_JOB + "min_bg_on_normal"),
-                        new MaxJobCounts(
-                                8, KEY_PREFIX_MAX_JOB + "total_on_moderate",
-                                4, KEY_PREFIX_MAX_JOB + "max_bg_on_moderate",
-                                2, KEY_PREFIX_MAX_JOB + "min_bg_on_moderate"),
-                        new MaxJobCounts(
-                                5, KEY_PREFIX_MAX_JOB + "total_on_low",
-                                1, KEY_PREFIX_MAX_JOB + "max_bg_on_low",
-                                1, KEY_PREFIX_MAX_JOB + "min_bg_on_low"),
-                        new MaxJobCounts(
-                                5, KEY_PREFIX_MAX_JOB + "total_on_critical",
-                                1, KEY_PREFIX_MAX_JOB + "max_bg_on_critical",
-                                1, KEY_PREFIX_MAX_JOB + "min_bg_on_critical"));
-
-        final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_OFF =
-                new MaxJobCountsPerMemoryTrimLevel(
-                        new MaxJobCounts(
-                                10, KEY_PREFIX_MAX_JOB + "total_off_normal",
-                                6, KEY_PREFIX_MAX_JOB + "max_bg_off_normal",
-                                2, KEY_PREFIX_MAX_JOB + "min_bg_off_normal"),
-                        new MaxJobCounts(
-                                10, KEY_PREFIX_MAX_JOB + "total_off_moderate",
-                                4, KEY_PREFIX_MAX_JOB + "max_bg_off_moderate",
-                                2, KEY_PREFIX_MAX_JOB + "min_bg_off_moderate"),
-                        new MaxJobCounts(
-                                5, KEY_PREFIX_MAX_JOB + "total_off_low",
-                                1, KEY_PREFIX_MAX_JOB + "max_bg_off_low",
-                                1, KEY_PREFIX_MAX_JOB + "min_bg_off_low"),
-                        new MaxJobCounts(
-                                5, KEY_PREFIX_MAX_JOB + "total_off_critical",
-                                1, KEY_PREFIX_MAX_JOB + "max_bg_off_critical",
-                                1, KEY_PREFIX_MAX_JOB + "min_bg_off_critical"));
-
         /**
          * The minimum backoff time to allow for linear backoff.
          */
@@ -686,18 +529,6 @@
                     DEFAULT_MODERATE_USE_FACTOR);
         }
 
-        void updateConcurrencyConstantsLocked() {
-            MAX_JOB_COUNTS_SCREEN_ON.normal.update();
-            MAX_JOB_COUNTS_SCREEN_ON.moderate.update();
-            MAX_JOB_COUNTS_SCREEN_ON.low.update();
-            MAX_JOB_COUNTS_SCREEN_ON.critical.update();
-
-            MAX_JOB_COUNTS_SCREEN_OFF.normal.update();
-            MAX_JOB_COUNTS_SCREEN_OFF.moderate.update();
-            MAX_JOB_COUNTS_SCREEN_OFF.low.update();
-            MAX_JOB_COUNTS_SCREEN_OFF.critical.update();
-        }
-
         private void updateBackoffConstantsLocked() {
             MIN_LINEAR_BACKOFF_TIME_MS = DeviceConfig.getLong(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
                     KEY_MIN_LINEAR_BACKOFF_TIME_MS,
@@ -747,16 +578,6 @@
             pw.print(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println();
             pw.print(KEY_MODERATE_USE_FACTOR, MODERATE_USE_FACTOR).println();
 
-            MAX_JOB_COUNTS_SCREEN_ON.normal.dump(pw, "");
-            MAX_JOB_COUNTS_SCREEN_ON.moderate.dump(pw, "");
-            MAX_JOB_COUNTS_SCREEN_ON.low.dump(pw, "");
-            MAX_JOB_COUNTS_SCREEN_ON.critical.dump(pw, "");
-
-            MAX_JOB_COUNTS_SCREEN_OFF.normal.dump(pw, "");
-            MAX_JOB_COUNTS_SCREEN_OFF.moderate.dump(pw, "");
-            MAX_JOB_COUNTS_SCREEN_OFF.low.dump(pw, "");
-            MAX_JOB_COUNTS_SCREEN_OFF.critical.dump(pw, "");
-
             pw.print(KEY_MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME_MS).println();
             pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println();
             pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
@@ -781,9 +602,6 @@
             proto.write(ConstantsProto.HEAVY_USE_FACTOR, HEAVY_USE_FACTOR);
             proto.write(ConstantsProto.MODERATE_USE_FACTOR, MODERATE_USE_FACTOR);
 
-            MAX_JOB_COUNTS_SCREEN_ON.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_ON);
-            MAX_JOB_COUNTS_SCREEN_OFF.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_OFF);
-
             proto.write(ConstantsProto.MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME_MS);
             proto.write(ConstantsProto.MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS);
             proto.write(ConstantsProto.CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 26b5abe..247b421 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -16,6 +16,7 @@
 
 package com.android.server.job;
 
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
 import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 
@@ -124,9 +125,12 @@
      *
      * Any reads (dereferences) not done from the handler thread must be synchronized on
      * {@link #mLock}.
-     * Writes can only be done from the handler thread, or {@link #executeRunnableJob(JobStatus)}.
+     * Writes can only be done from the handler thread,
+     * or {@link #executeRunnableJob(JobStatus, int)}.
      */
     private JobStatus mRunningJob;
+    @JobConcurrencyManager.WorkType
+    private int mRunningJobWorkType;
     private JobCallback mRunningCallback;
     /** Used to store next job to run when current job is to be preempted. */
     private int mPreferredUid;
@@ -181,30 +185,26 @@
 
     JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats,
             JobPackageTracker tracker, Looper looper) {
-        this(service.getContext(), service.getLock(), batteryStats, tracker, service, looper);
-    }
-
-    @VisibleForTesting
-    JobServiceContext(Context context, Object lock, IBatteryStats batteryStats,
-            JobPackageTracker tracker, JobCompletedListener completedListener, Looper looper) {
-        mContext = context;
-        mLock = lock;
+        mContext = service.getContext();
+        mLock = service.getLock();
         mBatteryStats = batteryStats;
         mJobPackageTracker = tracker;
         mCallbackHandler = new JobServiceHandler(looper);
-        mCompletedListener = completedListener;
+        mCompletedListener = service;
         mAvailable = true;
         mVerb = VERB_FINISHED;
         mPreferredUid = NO_PREFERRED_UID;
     }
 
     /**
-     * Give a job to this context for execution. Callers must first check {@link #getRunningJobLocked()}
+     * Give a job to this context for execution. Callers must first check {@link
+     * #getRunningJobLocked()}
      * and ensure it is null to make sure this is a valid context.
+     *
      * @param job The status of the job that we are going to run.
      * @return True if the job is valid and is running. False if the job cannot be executed.
      */
-    boolean executeRunnableJob(JobStatus job) {
+    boolean executeRunnableJob(JobStatus job, @JobConcurrencyManager.WorkType int workType) {
         synchronized (mLock) {
             if (!mAvailable) {
                 Slog.e(TAG, "Starting new runnable but context is unavailable > Error.");
@@ -214,6 +214,7 @@
             mPreferredUid = NO_PREFERRED_UID;
 
             mRunningJob = job;
+            mRunningJobWorkType = workType;
             mRunningCallback = new JobCallback();
             final boolean isDeadlineExpired =
                     job.hasDeadlineConstraint() &&
@@ -282,6 +283,7 @@
                     Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable.");
                 }
                 mRunningJob = null;
+                mRunningJobWorkType = WORK_TYPE_NONE;
                 mRunningCallback = null;
                 mParams = null;
                 mExecutionStartTimeElapsed = 0L;
@@ -326,6 +328,11 @@
         return mRunningJob;
     }
 
+    @JobConcurrencyManager.WorkType
+    int getRunningJobWorkType() {
+        return mRunningJobWorkType;
+    }
+
     /**
      * Used only for debugging. Will return <code>"&lt;null&gt;"</code> if there is no job running.
      */
@@ -831,6 +838,7 @@
         mContext.unbindService(JobServiceContext.this);
         mWakeLock = null;
         mRunningJob = null;
+        mRunningJobWorkType = WORK_TYPE_NONE;
         mRunningCallback = null;
         mParams = null;
         mVerb = VERB_FINISHED;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 09dc7d2..539c3c9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -305,6 +305,7 @@
     public Network network;
     public ServiceInfo serviceInfo;
 
+    /** The evaluated priority of the job when it started running. */
     public int lastEvaluatedPriority;
 
     // If non-null, this is work that has been enqueued for the job.
diff --git a/services/tests/servicestests/src/com/android/server/job/JobCountTrackerTest.java b/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java
similarity index 79%
rename from services/tests/servicestests/src/com/android/server/job/JobCountTrackerTest.java
rename to services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java
index e5529cb..15a9bcf 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobCountTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/WorkCountTrackerTest.java
@@ -16,36 +16,42 @@
 
 package com.android.server.job;
 
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.util.Log;
+import android.util.Pair;
 
-import com.android.server.job.JobConcurrencyManager.JobCountTracker;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.job.JobConcurrencyManager.WorkCountTracker;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
 import java.util.Random;
 
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
 /**
- * Test for {@link com.android.server.job.JobConcurrencyManager.JobCountTracker}.
+ * Test for {@link WorkCountTracker}.
  */
 @RunWith(AndroidJUnit4.class)
 @MediumTest
-public class JobCountTrackerTest {
-    private static final String TAG = "JobCountTrackerTest";
+public class WorkCountTrackerTest {
+    private static final String TAG = "WorkerCountTrackerTest";
 
     private Random mRandom;
-    private JobCountTracker mJobCountTracker;
+    private WorkCountTracker mWorkCountTracker;
 
     @Before
     public void setUp() {
         mRandom = new Random(1); // Always use the same series of pseudo random values.
-        mJobCountTracker = new JobCountTracker();
+        mWorkCountTracker = new WorkCountTracker();
     }
 
     /**
@@ -83,44 +89,57 @@
 
 
     private void startPendingJobs(Jobs jobs, int totalMax, int maxBg, int minBg) {
-        mJobCountTracker.reset(totalMax, maxBg, minBg);
+        mWorkCountTracker.setConfig(new JobConcurrencyManager.WorkTypeConfig("critical",
+                totalMax,
+                // defaultMin
+                List.of(Pair.create(WORK_TYPE_TOP, totalMax - maxBg),
+                        Pair.create(WORK_TYPE_BG, minBg)),
+                // defaultMax
+                List.of(Pair.create(WORK_TYPE_BG, maxBg))));
+        mWorkCountTracker.resetCounts();
 
         for (int i = 0; i < jobs.runningFg; i++) {
-            mJobCountTracker.incrementRunningJobCount(true);
+            mWorkCountTracker.incrementRunningJobCount(WORK_TYPE_TOP);
         }
         for (int i = 0; i < jobs.runningBg; i++) {
-            mJobCountTracker.incrementRunningJobCount(false);
+            mWorkCountTracker.incrementRunningJobCount(WORK_TYPE_BG);
         }
 
         for (int i = 0; i < jobs.pendingFg; i++) {
-            mJobCountTracker.incrementPendingJobCount(true);
+            mWorkCountTracker.incrementPendingJobCount(WORK_TYPE_TOP);
         }
         for (int i = 0; i < jobs.pendingBg; i++) {
-            mJobCountTracker.incrementPendingJobCount(false);
+            mWorkCountTracker.incrementPendingJobCount(WORK_TYPE_BG);
         }
 
-        mJobCountTracker.onCountDone();
+        mWorkCountTracker.onCountDone();
 
-        while ((jobs.pendingFg > 0 && mJobCountTracker.canJobStart(true))
-                || (jobs.pendingBg > 0 && mJobCountTracker.canJobStart(false))) {
+        while ((jobs.pendingFg > 0
+                && mWorkCountTracker.canJobStart(WORK_TYPE_TOP) != WORK_TYPE_NONE)
+                || (jobs.pendingBg > 0
+                && mWorkCountTracker.canJobStart(WORK_TYPE_BG) != WORK_TYPE_NONE)) {
             final boolean isStartingFg = mRandom.nextBoolean();
 
             if (isStartingFg) {
-                if (jobs.pendingFg > 0 && mJobCountTracker.canJobStart(true)) {
+                if (jobs.pendingFg > 0
+                        && mWorkCountTracker.canJobStart(WORK_TYPE_TOP) != WORK_TYPE_NONE) {
                     jobs.pendingFg--;
                     jobs.runningFg++;
-                    mJobCountTracker.onStartingNewJob(true);
+                    mWorkCountTracker.stageJob(WORK_TYPE_TOP);
+                    mWorkCountTracker.onJobStarted(WORK_TYPE_TOP);
                 }
             } else {
-                if (jobs.pendingBg > 0 && mJobCountTracker.canJobStart(false)) {
+                if (jobs.pendingBg > 0
+                        && mWorkCountTracker.canJobStart(WORK_TYPE_BG) != WORK_TYPE_NONE) {
                     jobs.pendingBg--;
                     jobs.runningBg++;
-                    mJobCountTracker.onStartingNewJob(false);
+                    mWorkCountTracker.stageJob(WORK_TYPE_BG);
+                    mWorkCountTracker.onJobStarted(WORK_TYPE_BG);
                 }
             }
         }
 
-        Log.i(TAG, "" + mJobCountTracker);
+        Log.i(TAG, "" + mWorkCountTracker);
     }
 
     /**
@@ -277,6 +296,7 @@
 
         startPendingJobs(jobs, totalMax, maxBg, minBg);
 
+//        fail(mWorkerCountTracker.toString());
         assertThat(jobs.runningFg).isEqualTo(resultRunningFg);
         assertThat(jobs.runningBg).isEqualTo(resultRunningBg);
 
@@ -300,6 +320,8 @@
         checkSimple(6, 4, 2, /*run=*/ 0, 0, /*pen=*/ 10, 1, /*res run/pen=*/ 5, 1, 5, 0);
         checkSimple(6, 4, 2, /*run=*/ 0, 0, /*pen=*/ 10, 3, /*res run/pen=*/ 4, 2, 6, 1);
 
+        checkSimple(8, 6, 2, /*run=*/ 0, 0, /*pen=*/ 0, 49, /*res run/pen=*/ 0, 6, 0, 43);
+
         checkSimple(6, 4, 2, /*run=*/ 6, 0, /*pen=*/ 10, 3, /*res run/pen=*/ 6, 0, 10, 3);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/job/MaxJobCountsTest.java b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java
similarity index 63%
rename from services/tests/servicestests/src/com/android/server/job/MaxJobCountsTest.java
rename to services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java
index 4c36747..fba36cb 100644
--- a/services/tests/servicestests/src/com/android/server/job/MaxJobCountsTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/WorkTypeConfigTest.java
@@ -15,22 +15,34 @@
  */
 package com.android.server.job;
 
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
+import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
+
 import android.annotation.Nullable;
 import android.provider.DeviceConfig;
+import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.server.job.JobSchedulerService.MaxJobCounts;
+import com.android.server.job.JobConcurrencyManager.WorkTypeConfig;
 
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.List;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
-public class MaxJobCountsTest {
+public class WorkTypeConfigTest {
+    private static final String KEY_MAX_TOTAL = "concurrency_max_total_test";
+    private static final String KEY_MAX_TOP = "concurrency_max_top_test";
+    private static final String KEY_MAX_BG = "concurrency_max_bg_test";
+    private static final String KEY_MIN_TOP = "concurrency_min_top_test";
+    private static final String KEY_MIN_BG = "concurrency_min_bg_test";
+
     @After
     public void tearDown() throws Exception {
         resetConfig();
@@ -38,9 +50,11 @@
 
     private void resetConfig() {
         // DeviceConfig.resetToDefaults() doesn't work here. Need to reset constants manually.
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, "total", "", false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, "maxbg", "", false);
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, "minbg", "", false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_TOTAL, "", false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_TOP, "", false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MAX_BG, "", false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_TOP, "", false);
+        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_MIN_BG, "", false);
     }
 
     private void check(@Nullable DeviceConfig.Properties config,
@@ -51,16 +65,19 @@
             DeviceConfig.setProperties(config);
         }
 
-        final MaxJobCounts counts = new JobSchedulerService.MaxJobCounts(
-                defaultTotal, "total",
-                defaultMaxBg, "maxbg",
-                defaultMinBg, "minbg");
+        final WorkTypeConfig counts = new WorkTypeConfig("test",
+                defaultTotal,
+                // defaultMin
+                List.of(Pair.create(WORK_TYPE_TOP, defaultTotal - defaultMaxBg),
+                        Pair.create(WORK_TYPE_BG, defaultMinBg)),
+                // defaultMax
+                List.of(Pair.create(WORK_TYPE_BG, defaultMaxBg)));
 
-        counts.update();
+        counts.update(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_JOB_SCHEDULER));
 
         Assert.assertEquals(expectedTotal, counts.getMaxTotal());
-        Assert.assertEquals(expectedMaxBg, counts.getMaxBg());
-        Assert.assertEquals(expectedMinBg, counts.getMinBg());
+        Assert.assertEquals(expectedMaxBg, counts.getMax(WORK_TYPE_BG));
+        Assert.assertEquals(expectedMinBg, counts.getMinReserved(WORK_TYPE_BG));
     }
 
     @Test
@@ -80,19 +97,19 @@
 
         // Test for overriding with a setting string.
         check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
-                        .setInt("total", 5)
-                        .setInt("maxbg", 4)
-                        .setInt("minbg", 3)
+                        .setInt(KEY_MAX_TOTAL, 5)
+                        .setInt(KEY_MAX_BG, 4)
+                        .setInt(KEY_MIN_BG, 3)
                         .build(),
                 /*default*/ 9, 9, 9, /*expected*/ 5, 4, 3);
         check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
-                        .setInt("total", 5).build(),
+                        .setInt(KEY_MAX_TOTAL, 5).build(),
                 /*default*/ 9, 9, 9, /*expected*/ 5, 5, 4);
         check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
-                        .setInt("maxbg", 4).build(),
+                        .setInt(KEY_MAX_BG, 4).build(),
                 /*default*/ 9, 9, 9, /*expected*/ 9, 4, 4);
         check(new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER)
-                        .setInt("minbg", 3).build(),
+                        .setInt(KEY_MIN_BG, 3).build(),
                 /*default*/ 9, 9, 9, /*expected*/ 9, 9, 3);
     }
 }