Place additional constraints on restricted jobs.

Update JobScheduler to place additional constraints on jobs in the
RESTRICTED bucket:
These jobs can only run when the device is idle, charging, battery is
not low, and on an unmetered network (for connectivity jobs). They do
get one parole session per day where they don't need these additional
constraints met.

Bug: 145551233
Test: atest CtsJobSchedulerTestCases
Change-Id: Ic96841141ed7800295b0b1a54c697a30afc3cd70
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index ef1351e..b96161a 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -48,6 +48,13 @@
     public static final int REASON_DEVICE_IDLE = JobProtoEnums.STOP_REASON_DEVICE_IDLE; // 4.
     /** @hide */
     public static final int REASON_DEVICE_THERMAL = JobProtoEnums.STOP_REASON_DEVICE_THERMAL; // 5.
+    /**
+     * The job is in the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
+     * bucket.
+     *
+     * @hide
+     */
+    public static final int REASON_RESTRAINED = JobProtoEnums.STOP_REASON_RESTRAINED; // 6.
 
     /**
      * All the stop reason codes. This should be regarded as an immutable array at runtime.
@@ -65,6 +72,7 @@
             REASON_TIMEOUT,
             REASON_DEVICE_IDLE,
             REASON_DEVICE_THERMAL,
+            REASON_RESTRAINED,
     };
 
     /**
@@ -80,6 +88,7 @@
             case REASON_TIMEOUT: return "timeout";
             case REASON_DEVICE_IDLE: return "device_idle";
             case REASON_DEVICE_THERMAL: return "thermal";
+            case REASON_RESTRAINED: return "restrained";
             default: return "unknown:" + reason;
         }
     }
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 102e848..e0c6d53 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -101,6 +101,7 @@
 import com.android.server.job.controllers.IdleController;
 import com.android.server.job.controllers.JobStatus;
 import com.android.server.job.controllers.QuotaController;
+import com.android.server.job.controllers.RestrictingController;
 import com.android.server.job.controllers.StateController;
 import com.android.server.job.controllers.StorageController;
 import com.android.server.job.controllers.TimeController;
@@ -241,6 +242,11 @@
 
     /** List of controllers that will notify this service of updates to jobs. */
     final List<StateController> mControllers;
+    /**
+     * List of controllers that will apply to all jobs in the RESTRICTED bucket. This is a subset of
+     * {@link #mControllers}.
+     */
+    private final List<RestrictingController> mRestrictiveControllers;
     /** Need direct access to this for testing. */
     private final BatteryController mBatteryController;
     /** Need direct access to this for testing. */
@@ -312,6 +318,9 @@
     public static final int FREQUENT_INDEX = 2;
     public static final int RARE_INDEX = 3;
     public static final int NEVER_INDEX = 4;
+    // Putting RESTRICTED_INDEX after NEVER_INDEX to make it easier for proto dumping
+    // (ScheduledJobStateChanged and JobStatusDumpProto).
+    public static final int RESTRICTED_INDEX = 5;
 
     // -- Pre-allocated temporaries only for use in assignJobsToContextsLocked --
 
@@ -1365,6 +1374,40 @@
         }
     }
 
+    @Override
+    public void onRestrictedBucketChanged(List<JobStatus> jobs) {
+        final int len = jobs.size();
+        if (len == 0) {
+            Slog.wtf(TAG, "onRestrictedBucketChanged called with no jobs");
+            return;
+        }
+        synchronized (mLock) {
+            for (int i = 0; i < len; ++i) {
+                JobStatus js = jobs.get(i);
+                for (int j = mRestrictiveControllers.size() - 1; j >= 0; --j) {
+                    // Effective standby bucket can change after this in some situations so use
+                    // the real bucket so that the job is tracked by the controllers.
+                    if (js.getStandbyBucket() == RESTRICTED_INDEX) {
+                        js.addDynamicConstraint(JobStatus.CONSTRAINT_BATTERY_NOT_LOW);
+                        js.addDynamicConstraint(JobStatus.CONSTRAINT_CHARGING);
+                        js.addDynamicConstraint(JobStatus.CONSTRAINT_CONNECTIVITY);
+                        js.addDynamicConstraint(JobStatus.CONSTRAINT_IDLE);
+
+                        mRestrictiveControllers.get(j).startTrackingRestrictedJobLocked(js);
+                    } else {
+                        js.removeDynamicConstraint(JobStatus.CONSTRAINT_BATTERY_NOT_LOW);
+                        js.removeDynamicConstraint(JobStatus.CONSTRAINT_CHARGING);
+                        js.removeDynamicConstraint(JobStatus.CONSTRAINT_CONNECTIVITY);
+                        js.removeDynamicConstraint(JobStatus.CONSTRAINT_IDLE);
+
+                        mRestrictiveControllers.get(j).stopTrackingRestrictedJobLocked(js);
+                    }
+                }
+            }
+        }
+        mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+    }
+
     void reportActiveLocked() {
         // active is true if pending queue contains jobs OR some job is running.
         boolean active = mPendingJobs.size() > 0;
@@ -1441,9 +1484,11 @@
 
         // Create the controllers.
         mControllers = new ArrayList<StateController>();
-        mControllers.add(new ConnectivityController(this));
+        final ConnectivityController connectivityController = new ConnectivityController(this);
+        mControllers.add(connectivityController);
         mControllers.add(new TimeController(this));
-        mControllers.add(new IdleController(this));
+        final IdleController idleController = new IdleController(this);
+        mControllers.add(idleController);
         mBatteryController = new BatteryController(this);
         mControllers.add(mBatteryController);
         mStorageController = new StorageController(this);
@@ -1455,6 +1500,11 @@
         mQuotaController = new QuotaController(this);
         mControllers.add(mQuotaController);
 
+        mRestrictiveControllers = new ArrayList<>();
+        mRestrictiveControllers.add(mBatteryController);
+        mRestrictiveControllers.add(connectivityController);
+        mRestrictiveControllers.add(idleController);
+
         // Create restrictions
         mJobRestrictions = new ArrayList<>();
         mJobRestrictions.add(new ThermalStatusRestriction(this));
@@ -2125,11 +2175,13 @@
                     }
                 } catch (RemoteException e) {
                 }
-                if (mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
+                // Restricted jobs must always be batched
+                if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX
+                        || (mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
                         && job.getEffectiveStandbyBucket() != ACTIVE_INDEX
                         && (job.getFirstForceBatchedTimeElapsed() == 0
                         || sElapsedRealtimeClock.millis() - job.getFirstForceBatchedTimeElapsed()
-                                < mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS)) {
+                                < mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS))) {
                     // Force batching non-ACTIVE jobs. Don't include them in the other counts.
                     forceBatchedCount++;
                     if (job.getFirstForceBatchedTimeElapsed() == 0) {
@@ -2536,11 +2588,19 @@
 
     public static int standbyBucketToBucketIndex(int bucket) {
         // Normalize AppStandby constants to indices into our bookkeeping
-        if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) return NEVER_INDEX;
-        else if (bucket > UsageStatsManager.STANDBY_BUCKET_FREQUENT) return RARE_INDEX;
-        else if (bucket > UsageStatsManager.STANDBY_BUCKET_WORKING_SET) return FREQUENT_INDEX;
-        else if (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE) return WORKING_INDEX;
-        else return ACTIVE_INDEX;
+        if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) {
+            return NEVER_INDEX;
+        } else if (bucket > UsageStatsManager.STANDBY_BUCKET_RARE) {
+            return RESTRICTED_INDEX;
+        } else if (bucket > UsageStatsManager.STANDBY_BUCKET_FREQUENT) {
+            return RARE_INDEX;
+        } else if (bucket > UsageStatsManager.STANDBY_BUCKET_WORKING_SET) {
+            return FREQUENT_INDEX;
+        } else if (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE) {
+            return WORKING_INDEX;
+        } else {
+            return ACTIVE_INDEX;
+        }
     }
 
     // Static to support external callers
diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
index 87bfc27..cb3c437 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
@@ -16,8 +16,12 @@
 
 package com.android.server.job;
 
+import android.annotation.NonNull;
+
 import com.android.server.job.controllers.JobStatus;
 
+import java.util.List;
+
 /**
  * Interface through which a {@link com.android.server.job.controllers.StateController} informs
  * the {@link com.android.server.job.JobSchedulerService} that there are some tasks potentially
@@ -39,4 +43,10 @@
     public void onRunJobNow(JobStatus jobStatus);
 
     public void onDeviceIdleStateChanged(boolean deviceIdle);
+
+    /**
+     * Called when these jobs are added or removed from the
+     * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+     */
+    void onRestrictedBucketChanged(@NonNull List<JobStatus> jobs);
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index 46658ad..461ef21 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -43,7 +43,7 @@
  * be charging when it's been plugged in for more than two minutes, and the system has broadcast
  * ACTION_BATTERY_OK.
  */
-public final class BatteryController extends StateController {
+public final class BatteryController extends RestrictingController {
     private static final String TAG = "JobScheduler.Battery";
     private static final boolean DEBUG = JobSchedulerService.DEBUG
             || Log.isLoggable(TAG, Log.DEBUG);
@@ -73,12 +73,24 @@
     }
 
     @Override
+    public void startTrackingRestrictedJobLocked(JobStatus jobStatus) {
+        maybeStartTrackingJobLocked(jobStatus, null);
+    }
+
+    @Override
     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
             mTrackedTasks.remove(taskStatus);
         }
     }
 
+    @Override
+    public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
+        if (!jobStatus.hasPowerConstraint()) {
+            maybeStopTrackingJobLocked(jobStatus, null, false);
+        }
+    }
+
     private void maybeReportNewChargingStateLocked() {
         final boolean stablePower = mChargeTracker.isOnStablePower();
         final boolean batteryNotLow = mChargeTracker.isBatteryNotLow();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 8eeea1b..a0e83da 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -20,6 +20,8 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 
+import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
+
 import android.app.job.JobInfo;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
@@ -63,7 +65,7 @@
  *
  * Test: atest com.android.server.job.controllers.ConnectivityControllerTest
  */
-public final class ConnectivityController extends StateController implements
+public final class ConnectivityController extends RestrictingController implements
         ConnectivityManager.OnNetworkActiveListener {
     private static final String TAG = "JobScheduler.Connectivity";
     private static final boolean DEBUG = JobSchedulerService.DEBUG
@@ -138,8 +140,22 @@
         }
     }
 
+    @Override
+    public void startTrackingRestrictedJobLocked(JobStatus jobStatus) {
+        // Don't need to start tracking the job. If the job needed network, it would already be
+        // tracked.
+        updateConstraintsSatisfied(jobStatus);
+    }
+
+    @Override
+    public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
+        // Shouldn't stop tracking the job here. If the job was tracked, it still needs network,
+        // even after being unrestricted.
+        updateConstraintsSatisfied(jobStatus);
+    }
+
     /**
-     * Returns true if the job's requested network is available. This DOES NOT necesarilly mean
+     * Returns true if the job's requested network is available. This DOES NOT necessarily mean
      * that the UID has been granted access to the network.
      */
     public boolean isNetworkAvailable(JobStatus job) {
@@ -353,14 +369,24 @@
 
     private static boolean isStrictSatisfied(JobStatus jobStatus, Network network,
             NetworkCapabilities capabilities, Constants constants) {
-        return jobStatus.getJob().getRequiredNetwork().networkCapabilities
-                .satisfiedByNetworkCapabilities(capabilities);
+        final NetworkCapabilities required;
+        // A restricted job that's out of quota MUST use an unmetered network.
+        if (jobStatus.getEffectiveStandbyBucket() == RESTRICTED_INDEX
+                && !jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
+            required = new NetworkCapabilities(
+                    jobStatus.getJob().getRequiredNetwork().networkCapabilities)
+                    .addCapability(NET_CAPABILITY_NOT_METERED);
+        } else {
+            required = jobStatus.getJob().getRequiredNetwork().networkCapabilities;
+        }
+
+        return required.satisfiedByNetworkCapabilities(capabilities);
     }
 
     private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network,
             NetworkCapabilities capabilities, Constants constants) {
-        // Only consider doing this for prefetching jobs
-        if (!jobStatus.getJob().isPrefetch()) {
+        // Only consider doing this for unrestricted prefetching jobs
+        if (!jobStatus.getJob().isPrefetch() || jobStatus.getStandbyBucket() == RESTRICTED_INDEX) {
             return false;
         }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
index d355715..c0b3204 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -32,7 +32,15 @@
 
 import java.util.function.Predicate;
 
-public final class IdleController extends StateController implements IdlenessListener {
+/**
+ * Simple controller that tracks whether the device is idle or not. Idleness depends on the device
+ * type and is not related to device-idle (Doze mode) despite the similar naming.
+ *
+ * @see CarIdlenessTracker
+ * @see DeviceIdlenessTracker
+ * @see IdlenessTracker
+ */
+public final class IdleController extends RestrictingController implements IdlenessListener {
     private static final String TAG = "JobScheduler.IdleController";
     // Policy: we decide that we're "idle" if the device has been unused /
     // screen off or dreaming or wireless charging dock idle for at least this long
@@ -57,6 +65,11 @@
     }
 
     @Override
+    public void startTrackingRestrictedJobLocked(JobStatus jobStatus) {
+        maybeStartTrackingJobLocked(jobStatus, null);
+    }
+
+    @Override
     public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
             boolean forUpdate) {
         if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) {
@@ -64,6 +77,13 @@
         }
     }
 
+    @Override
+    public void stopTrackingRestrictedJobLocked(JobStatus jobStatus) {
+        if (!jobStatus.hasIdleConstraint()) {
+            maybeStopTrackingJobLocked(jobStatus, null, false);
+        }
+    }
+
     /**
      * State-change notifications from the idleness tracker
      */
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 a8d8bd9..dbdce70 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
@@ -69,13 +69,14 @@
     public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
     public static final long NO_EARLIEST_RUNTIME = 0L;
 
-    static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
-    static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
-    static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
+    public static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
+    public static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
+    public static final int CONSTRAINT_BATTERY_NOT_LOW =
+            JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
     static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
     static final int CONSTRAINT_TIMING_DELAY = 1<<31;
     static final int CONSTRAINT_DEADLINE = 1<<30;
-    static final int CONSTRAINT_CONNECTIVITY = 1<<28;
+    public static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
     static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
     static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
     static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
@@ -117,7 +118,7 @@
     /** The minimum possible update delay is 1/2 second. */
     public static final long MIN_TRIGGER_UPDATE_DELAY = 500;
 
-    /** If not specified, trigger maxumum delay is 2 minutes. */
+    /** If not specified, trigger maximum delay is 2 minutes. */
     public static final long DEFAULT_TRIGGER_MAX_DELAY = 2*60*1000;
 
     /** The minimum possible update delay is 1 second. */
@@ -188,6 +189,11 @@
     private final int mRequiredConstraintsOfInterest;
     int satisfiedConstraints = 0;
     private int mSatisfiedConstraintsOfInterest = 0;
+    /**
+     * Set of constraints that must be satisfied for the job if/because it's in the RESTRICTED
+     * bucket.
+     */
+    private int mDynamicConstraints = 0;
 
     // Set to true if doze constraint was satisfied due to app being whitelisted.
     public boolean dozeWhitelisted;
@@ -328,6 +334,9 @@
     /** The job is within its quota based on its standby bucket. */
     private boolean mReadyWithinQuota;
 
+    /** The job's dynamic requirements have been satisfied. */
+    private boolean mReadyDynamicSatisfied;
+
     /** Provide a handle to the service that this job will be run on. */
     public int getServiceToken() {
         return callingUid;
@@ -410,6 +419,7 @@
         this.requiredConstraints = requiredConstraints;
         mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
         mReadyNotDozing = (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+        mReadyDynamicSatisfied = true;
 
         mLastSuccessfulRunTime = lastSuccessfulRunTime;
         mLastFailedRunTime = lastFailedRunTime;
@@ -830,41 +840,54 @@
 
     /** Does this job have any sort of networking constraint? */
     public boolean hasConnectivityConstraint() {
+        // No need to check mDynamicConstraints since connectivity will only be in that list if
+        // it's already in the requiredConstraints list.
         return (requiredConstraints&CONSTRAINT_CONNECTIVITY) != 0;
     }
 
     public boolean hasChargingConstraint() {
-        return (requiredConstraints&CONSTRAINT_CHARGING) != 0;
+        return hasConstraint(CONSTRAINT_CHARGING);
     }
 
     public boolean hasBatteryNotLowConstraint() {
-        return (requiredConstraints&CONSTRAINT_BATTERY_NOT_LOW) != 0;
+        return hasConstraint(CONSTRAINT_BATTERY_NOT_LOW);
     }
 
-    public boolean hasPowerConstraint() {
-        return (requiredConstraints&(CONSTRAINT_CHARGING|CONSTRAINT_BATTERY_NOT_LOW)) != 0;
+    /** Returns true if the job requires charging OR battery not low. */
+    boolean hasPowerConstraint() {
+        return hasConstraint(CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW);
     }
 
     public boolean hasStorageNotLowConstraint() {
-        return (requiredConstraints&CONSTRAINT_STORAGE_NOT_LOW) != 0;
+        return hasConstraint(CONSTRAINT_STORAGE_NOT_LOW);
     }
 
     public boolean hasTimingDelayConstraint() {
-        return (requiredConstraints&CONSTRAINT_TIMING_DELAY) != 0;
+        return hasConstraint(CONSTRAINT_TIMING_DELAY);
     }
 
     public boolean hasDeadlineConstraint() {
-        return (requiredConstraints&CONSTRAINT_DEADLINE) != 0;
+        return hasConstraint(CONSTRAINT_DEADLINE);
     }
 
     public boolean hasIdleConstraint() {
-        return (requiredConstraints&CONSTRAINT_IDLE) != 0;
+        return hasConstraint(CONSTRAINT_IDLE);
     }
 
     public boolean hasContentTriggerConstraint() {
+        // No need to check mDynamicConstraints since content trigger will only be in that list if
+        // it's already in the requiredConstraints list.
         return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0;
     }
 
+    /**
+     * Checks both {@link #requiredConstraints} and {@link #mDynamicConstraints} to see if this job
+     * requires the specified constraint.
+     */
+    private boolean hasConstraint(int constraint) {
+        return (requiredConstraints & constraint) != 0 || (mDynamicConstraints & constraint) != 0;
+    }
+
     public long getTriggerContentUpdateDelay() {
         long time = job.getTriggerContentUpdateDelay();
         if (time < 0) {
@@ -1033,6 +1056,8 @@
         }
         satisfiedConstraints = (satisfiedConstraints&~constraint) | (state ? constraint : 0);
         mSatisfiedConstraintsOfInterest = satisfiedConstraints & CONSTRAINTS_OF_INTEREST;
+        mReadyDynamicSatisfied =
+                mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
         if (STATS_LOG_ENABLED && (STATSD_CONSTRAINTS_TO_LOG & constraint) != 0) {
             StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED,
                     sourceUid, null, getBatteryName(), getProtoConstraint(constraint),
@@ -1058,6 +1083,43 @@
         trackingControllers |= which;
     }
 
+    /**
+     * Indicates that this job cannot run without the specified constraint. This is evaluated
+     * separately from the job's explicitly requested constraints and MUST be satisfied before
+     * the job can run if the app doesn't have quota.
+     *
+     */
+    public void addDynamicConstraint(int constraint) {
+        if (constraint == CONSTRAINT_WITHIN_QUOTA) {
+            Slog.wtf(TAG, "Tried to set quota as a dynamic constraint");
+            return;
+        }
+
+        // Connectivity and content trigger are special since they're only valid to add if the
+        // job has requested network or specific content URIs. Adding these constraints to jobs
+        // that don't need them doesn't make sense.
+        if ((constraint == CONSTRAINT_CONNECTIVITY && !hasConnectivityConstraint())
+                || (constraint == CONSTRAINT_CONTENT_TRIGGER && !hasContentTriggerConstraint())) {
+            return;
+        }
+
+        mDynamicConstraints |= constraint;
+        mReadyDynamicSatisfied =
+                mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+    }
+
+    /**
+     * Removes a dynamic constraint from a job, meaning that the requirement is not required for
+     * the job to run (if the job itself hasn't requested the constraint. This is separate from
+     * the job's explicitly requested constraints and does not remove those requested constraints.
+     *
+     */
+    public void removeDynamicConstraint(int constraint) {
+        mDynamicConstraints &= ~constraint;
+        mReadyDynamicSatisfied =
+                mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+    }
+
     public long getLastSuccessfulRunTime() {
         return mLastSuccessfulRunTime;
     }
@@ -1099,6 +1161,8 @@
                 break;
             default:
                 satisfied |= constraint;
+                mReadyDynamicSatisfied =
+                        mDynamicConstraints == (satisfied & mDynamicConstraints);
                 break;
         }
 
@@ -1117,24 +1181,29 @@
             case CONSTRAINT_WITHIN_QUOTA:
                 mReadyWithinQuota = oldValue;
                 break;
+            default:
+                mReadyDynamicSatisfied =
+                        mDynamicConstraints == (satisfiedConstraints & mDynamicConstraints);
+                break;
         }
         return toReturn;
     }
 
     private boolean isReady(int satisfiedConstraints) {
-        // Quota constraints trumps all other constraints.
-        if (!mReadyWithinQuota) {
+        // Quota and dynamic constraints trump all other constraints.
+        if (!mReadyWithinQuota && !mReadyDynamicSatisfied) {
             return false;
         }
-        // Deadline constraint trumps other constraints besides quota (except for periodic jobs
-        // where deadline is an implementation detail. A periodic job should only run if its
-        // constraints are satisfied).
+        // Deadline constraint trumps other constraints besides quota and dynamic (except for
+        // periodic jobs where deadline is an implementation detail. A periodic job should only
+        // run if its constraints are satisfied).
         // DeviceNotDozing implicit constraint must be satisfied
         // NotRestrictedInBackground implicit constraint must be satisfied
         return mReadyNotDozing && mReadyNotRestrictedInBg && (mReadyDeadlineSatisfied
                 || isConstraintsSatisfied(satisfiedConstraints));
     }
 
+    /** All constraints besides implicit and deadline. */
     static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
             | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY
             | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER;
@@ -1441,6 +1510,8 @@
             case 2: return "FREQUENT";
             case 3: return "RARE";
             case 4: return "NEVER";
+            case 5:
+                return "RESTRICTED";
             default:
                 return "Unknown: " + standbyBucket;
         }
@@ -1560,6 +1631,10 @@
         pw.print(prefix); pw.print("Required constraints:");
         dumpConstraints(pw, requiredConstraints);
         pw.println();
+        pw.print(prefix);
+        pw.print("Dynamic constraints:");
+        dumpConstraints(pw, mDynamicConstraints);
+        pw.println();
         if (full) {
             pw.print(prefix); pw.print("Satisfied constraints:");
             dumpConstraints(pw, satisfiedConstraints);
@@ -1599,6 +1674,9 @@
             pw.print(prefix); pw.print("  readyDeadlineSatisfied: ");
             pw.println(mReadyDeadlineSatisfied);
         }
+        pw.print(prefix);
+        pw.print("  readyDynamicSatisfied: ");
+        pw.println(mReadyDynamicSatisfied);
 
         if (changedAuthorities != null) {
             pw.print(prefix); pw.println("Changed authorities:");
@@ -1760,6 +1838,7 @@
         }
 
         dumpConstraints(proto, JobStatusDumpProto.REQUIRED_CONSTRAINTS, requiredConstraints);
+        dumpConstraints(proto, JobStatusDumpProto.DYNAMIC_CONSTRAINTS, mDynamicConstraints);
         if (full) {
             dumpConstraints(proto, JobStatusDumpProto.SATISFIED_CONSTRAINTS, satisfiedConstraints);
             dumpConstraints(proto, JobStatusDumpProto.UNSATISFIED_CONSTRAINTS,
@@ -1807,6 +1886,8 @@
                 mReadyNotRestrictedInBg);
         // mReadyDeadlineSatisfied isn't an implicit constraint...and can be determined from other
         // field values.
+        proto.write(JobStatusDumpProto.ImplicitConstraints.IS_DYNAMIC_SATISFIED,
+                mReadyDynamicSatisfied);
         proto.end(icToken);
 
         if (changedAuthorities != null) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 2e735a4..8eefac8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -24,6 +24,7 @@
 import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
 import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
 import static com.android.server.job.JobSchedulerService.RARE_INDEX;
+import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 
@@ -424,7 +425,9 @@
             QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
             QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
             QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
-            QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS
+            QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS,
+            0, // NEVER
+            QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS
     };
 
     /** The maximum period any bucket can have. */
@@ -441,7 +444,9 @@
             QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE,
             QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING,
             QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT,
-            QcConstants.DEFAULT_MAX_JOB_COUNT_RARE
+            QcConstants.DEFAULT_MAX_JOB_COUNT_RARE,
+            0, // NEVER
+            QcConstants.DEFAULT_MAX_JOB_COUNT_RESTRICTED
     };
 
     /**
@@ -455,7 +460,9 @@
             QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE,
             QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING,
             QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT,
-            QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE
+            QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE,
+            0, // NEVER
+            QcConstants.DEFAULT_MAX_SESSION_COUNT_RESTRICTED,
     };
 
     /**
@@ -648,7 +655,11 @@
 
         // Quota constraint is not enforced while charging.
         if (mChargeTracker.isCharging()) {
-            return true;
+            // Restricted jobs require additional constraints when charging, so don't immediately
+            // mark quota as free when charging.
+            if (standbyBucket != RESTRICTED_INDEX) {
+                return true;
+            }
         }
 
         ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
@@ -1105,14 +1116,37 @@
         }
     }
 
+    private class TimerChargingUpdateFunctor implements Consumer<Timer> {
+        private long mNowElapsed;
+        private boolean mIsCharging;
+
+        private void setStatus(long nowElapsed, boolean isCharging) {
+            mNowElapsed = nowElapsed;
+            mIsCharging = isCharging;
+        }
+
+        @Override
+        public void accept(Timer timer) {
+            if (JobSchedulerService.standbyBucketForPackage(timer.mPkg.packageName,
+                    timer.mPkg.userId, mNowElapsed) != RESTRICTED_INDEX) {
+                // Restricted jobs need additional constraints even when charging, so don't
+                // immediately say that quota is free.
+                timer.onStateChangedLocked(mNowElapsed, mIsCharging);
+            }
+        }
+    }
+
+    private final TimerChargingUpdateFunctor
+            mTimerChargingUpdateFunctor = new TimerChargingUpdateFunctor();
+
     private void handleNewChargingStateLocked() {
-        final long nowElapsed = sElapsedRealtimeClock.millis();
-        final boolean isCharging = mChargeTracker.isCharging();
+        mTimerChargingUpdateFunctor.setStatus(sElapsedRealtimeClock.millis(),
+                mChargeTracker.isCharging());
         if (DEBUG) {
-            Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging);
+            Slog.d(TAG, "handleNewChargingStateLocked: " + mChargeTracker.isCharging());
         }
         // Deal with Timers first.
-        mPkgTimers.forEach((t) -> t.onStateChangedLocked(nowElapsed, isCharging));
+        mPkgTimers.forEach(mTimerChargingUpdateFunctor);
         // Now update jobs.
         maybeUpdateAllConstraintsLocked();
     }
@@ -1555,7 +1589,10 @@
         }
 
         private boolean shouldTrackLocked() {
-            return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid);
+            final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName,
+                    mPkg.userId, sElapsedRealtimeClock.millis());
+            return (standbyBucket == RESTRICTED_INDEX || !mChargeTracker.isCharging())
+                    && !mForegroundUids.get(mUid);
         }
 
         void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) {
@@ -1670,6 +1707,7 @@
                     Slog.i(TAG, "Moving pkg " + string(userId, packageName) + " to bucketIndex "
                             + bucketIndex);
                 }
+                List<JobStatus> restrictedChanges = new ArrayList<>();
                 synchronized (mLock) {
                     ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
                     if (jobs == null || jobs.size() == 0) {
@@ -1677,6 +1715,13 @@
                     }
                     for (int i = jobs.size() - 1; i >= 0; i--) {
                         JobStatus js = jobs.valueAt(i);
+                        // Effective standby bucket can change after this in some situations so
+                        // use the real bucket so that the job is tracked by the controllers.
+                        if ((bucketIndex == RESTRICTED_INDEX
+                                || js.getStandbyBucket() == RESTRICTED_INDEX)
+                                && bucketIndex != js.getStandbyBucket()) {
+                            restrictedChanges.add(js);
+                        }
                         js.setStandbyBucket(bucketIndex);
                     }
                     Timer timer = mPkgTimers.get(userId, packageName);
@@ -1687,6 +1732,9 @@
                         mStateChangedListener.onControllerStateChanged();
                     }
                 }
+                if (restrictedChanges.size() > 0) {
+                    mStateChangedListener.onRestrictedBucketChanged(restrictedChanges);
+                }
             });
         }
     }
@@ -1863,11 +1911,13 @@
         private static final String KEY_WINDOW_SIZE_WORKING_MS = "window_size_working_ms";
         private static final String KEY_WINDOW_SIZE_FREQUENT_MS = "window_size_frequent_ms";
         private static final String KEY_WINDOW_SIZE_RARE_MS = "window_size_rare_ms";
+        private static final String KEY_WINDOW_SIZE_RESTRICTED_MS = "window_size_restricted_ms";
         private static final String KEY_MAX_EXECUTION_TIME_MS = "max_execution_time_ms";
         private static final String KEY_MAX_JOB_COUNT_ACTIVE = "max_job_count_active";
         private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working";
         private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent";
         private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare";
+        private static final String KEY_MAX_JOB_COUNT_RESTRICTED = "max_job_count_restricted";
         private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms";
         private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
                 "max_job_count_per_rate_limiting_window";
@@ -1875,6 +1925,8 @@
         private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working";
         private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent";
         private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare";
+        private static final String KEY_MAX_SESSION_COUNT_RESTRICTED =
+                "max_session_count_restricted";
         private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
                 "max_session_count_per_rate_limiting_window";
         private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS =
@@ -1892,6 +1944,8 @@
                 8 * 60 * 60 * 1000L; // 8 hours
         private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
                 24 * 60 * 60 * 1000L; // 24 hours
+        private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS =
+                24 * 60 * 60 * 1000L; // 24 hours
         private static final long DEFAULT_MAX_EXECUTION_TIME_MS =
                 4 * HOUR_IN_MILLIS;
         private static final long DEFAULT_RATE_LIMITING_WINDOW_MS =
@@ -1905,6 +1959,7 @@
                 (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
         private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
                 (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
+        private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10;
         private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE =
                 75; // 450/hr
         private static final int DEFAULT_MAX_SESSION_COUNT_WORKING =
@@ -1913,6 +1968,7 @@
                 8; // 1/hr
         private static final int DEFAULT_MAX_SESSION_COUNT_RARE =
                 3; // .125/hr
+        private static final int DEFAULT_MAX_SESSION_COUNT_RESTRICTED = 1; // 1/day
         private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
         private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
 
@@ -1954,6 +2010,13 @@
         public long WINDOW_SIZE_RARE_MS = DEFAULT_WINDOW_SIZE_RARE_MS;
 
         /**
+         * The quota window size of the particular standby bucket. Apps in this standby bucket are
+         * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
+         * WINDOW_SIZE_MS.
+         */
+        public long WINDOW_SIZE_RESTRICTED_MS = DEFAULT_WINDOW_SIZE_RESTRICTED_MS;
+
+        /**
          * The maximum amount of time an app can have its jobs running within a 24 hour window.
          */
         public long MAX_EXECUTION_TIME_MS = DEFAULT_MAX_EXECUTION_TIME_MS;
@@ -1982,6 +2045,12 @@
          */
         public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE;
 
+        /**
+         * The maximum number of jobs an app can run within this particular standby bucket's
+         * window size.
+         */
+        public int MAX_JOB_COUNT_RESTRICTED = DEFAULT_MAX_JOB_COUNT_RESTRICTED;
+
         /** The period of time used to rate limit recently run jobs. */
         public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS;
 
@@ -2016,6 +2085,12 @@
         public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE;
 
         /**
+         * The maximum number of {@link TimingSession}s an app can run within this particular
+         * standby bucket's window size.
+         */
+        public int MAX_SESSION_COUNT_RESTRICTED = DEFAULT_MAX_SESSION_COUNT_RESTRICTED;
+
+        /**
          * The maximum number of {@link TimingSession}s that can run within the past
          * {@link #ALLOWED_TIME_PER_PERIOD_MS}.
          */
@@ -2087,6 +2162,8 @@
                     KEY_WINDOW_SIZE_FREQUENT_MS, DEFAULT_WINDOW_SIZE_FREQUENT_MS);
             WINDOW_SIZE_RARE_MS = mParser.getDurationMillis(
                     KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS);
+            WINDOW_SIZE_RESTRICTED_MS = mParser.getDurationMillis(
+                    KEY_WINDOW_SIZE_RESTRICTED_MS, DEFAULT_WINDOW_SIZE_RESTRICTED_MS);
             MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
                     KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS);
             MAX_JOB_COUNT_ACTIVE = mParser.getInt(
@@ -2097,6 +2174,8 @@
                     KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT);
             MAX_JOB_COUNT_RARE = mParser.getInt(
                     KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE);
+            MAX_JOB_COUNT_RESTRICTED = mParser.getInt(
+                    KEY_MAX_JOB_COUNT_RESTRICTED, DEFAULT_MAX_JOB_COUNT_RESTRICTED);
             RATE_LIMITING_WINDOW_MS = mParser.getLong(
                     KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS);
             MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
@@ -2110,6 +2189,8 @@
                     KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT);
             MAX_SESSION_COUNT_RARE = mParser.getInt(
                     KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE);
+            MAX_SESSION_COUNT_RESTRICTED = mParser.getInt(
+                    KEY_MAX_SESSION_COUNT_RESTRICTED, DEFAULT_MAX_SESSION_COUNT_RESTRICTED);
             MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
                     KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
                     DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
@@ -2173,6 +2254,13 @@
                     mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
                     changed = true;
                 }
+                // Fit in the range [allowed time (10 mins), 1 week].
+                long newRestrictedPeriodMs = Math.max(mAllowedTimePerPeriodMs,
+                        Math.min(7 * 24 * 60 * MINUTE_IN_MILLIS, WINDOW_SIZE_RESTRICTED_MS));
+                if (mBucketPeriodsMs[RESTRICTED_INDEX] != newRestrictedPeriodMs) {
+                    mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs;
+                    changed = true;
+                }
                 long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS,
                         Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS));
                 if (mRateLimitingWindowMs != newRateLimitingWindowMs) {
@@ -2206,6 +2294,12 @@
                     mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
                     changed = true;
                 }
+                int newRestrictedMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT,
+                        MAX_JOB_COUNT_RESTRICTED);
+                if (mMaxBucketJobCounts[RESTRICTED_INDEX] != newRestrictedMaxJobCount) {
+                    mMaxBucketJobCounts[RESTRICTED_INDEX] = newRestrictedMaxJobCount;
+                    changed = true;
+                }
                 int newMaxSessionCountPerRateLimitPeriod = Math.max(
                         MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
                         MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
@@ -2237,6 +2331,11 @@
                     mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount;
                     changed = true;
                 }
+                int newRestrictedMaxSessionCount = Math.max(0, MAX_SESSION_COUNT_RESTRICTED);
+                if (mMaxBucketSessionCounts[RESTRICTED_INDEX] != newRestrictedMaxSessionCount) {
+                    mMaxBucketSessionCounts[RESTRICTED_INDEX] = newRestrictedMaxSessionCount;
+                    changed = true;
+                }
                 long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS,
                         Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS));
                 if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) {
@@ -2266,11 +2365,13 @@
             pw.printPair(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println();
             pw.printPair(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println();
             pw.printPair(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println();
+            pw.printPair(KEY_WINDOW_SIZE_RESTRICTED_MS, WINDOW_SIZE_RESTRICTED_MS).println();
             pw.printPair(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println();
             pw.printPair(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println();
             pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
             pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
             pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
+            pw.printPair(KEY_MAX_JOB_COUNT_RESTRICTED, MAX_JOB_COUNT_RESTRICTED).println();
             pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
             pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
                     MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println();
@@ -2278,6 +2379,7 @@
             pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
+            pw.printPair(KEY_MAX_SESSION_COUNT_RESTRICTED, MAX_SESSION_COUNT_RESTRICTED).println();
             pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
                     MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
             pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
@@ -2297,6 +2399,8 @@
             proto.write(ConstantsProto.QuotaController.FREQUENT_WINDOW_SIZE_MS,
                     WINDOW_SIZE_FREQUENT_MS);
             proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, WINDOW_SIZE_RARE_MS);
+            proto.write(ConstantsProto.QuotaController.RESTRICTED_WINDOW_SIZE_MS,
+                    WINDOW_SIZE_RESTRICTED_MS);
             proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
                     MAX_EXECUTION_TIME_MS);
             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE);
@@ -2305,6 +2409,8 @@
             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
                     MAX_JOB_COUNT_FREQUENT);
             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE);
+            proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RESTRICTED,
+                    MAX_JOB_COUNT_RESTRICTED);
             proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS,
                     RATE_LIMITING_WINDOW_MS);
             proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
@@ -2317,6 +2423,8 @@
                     MAX_SESSION_COUNT_FREQUENT);
             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE,
                     MAX_SESSION_COUNT_RARE);
+            proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RESTRICTED,
+                    MAX_SESSION_COUNT_RESTRICTED);
             proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
                     MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
             proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS,
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java
new file mode 100644
index 0000000..5c637bb
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job.controllers;
+
+import com.android.server.job.JobSchedulerService;
+
+/**
+ * Controller that can also handle jobs in the
+ * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+ */
+public abstract class RestrictingController extends StateController {
+    RestrictingController(JobSchedulerService service) {
+        super(service);
+    }
+
+    /**
+     * Start tracking a job that has been added to the
+     * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+     */
+    public abstract void startTrackingRestrictedJobLocked(JobStatus jobStatus);
+
+    /**
+     * Stop tracking a job that has been removed from the
+     * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
+     */
+    public abstract void stopTrackingRestrictedJobLocked(JobStatus jobStatus);
+}
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 4372e22..f36bebb 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -810,6 +810,7 @@
         FREQUENT = 2;
         RARE = 3;
         NEVER = 4;
+        RESTRICTED = 5;
     }
     optional Bucket standby_bucket = 5 [default = UNKNOWN];
 
diff --git a/core/proto/android/app/job/enums.proto b/core/proto/android/app/job/enums.proto
index f702b3e..d2bf205 100644
--- a/core/proto/android/app/job/enums.proto
+++ b/core/proto/android/app/job/enums.proto
@@ -34,4 +34,5 @@
     STOP_REASON_TIMEOUT = 3;
     STOP_REASON_DEVICE_IDLE = 4;
     STOP_REASON_DEVICE_THERMAL = 5;
+    STOP_REASON_RESTRAINED = 6;
 }
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index b71e539..303d62d 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -282,6 +282,10 @@
         // expected to run only {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS} within the past
         // WINDOW_SIZE_MS.
         optional int64 rare_window_size_ms = 6;
+        // The quota window size of the particular standby bucket. Apps in this standby bucket are
+        // expected to run only {@link QUOTA_CONTROLLER_ALLOWED_TIME_PER_PERIOD_MS} within the past
+        // WINDOW_SIZE_MS.
+        optional int64 restricted_window_size_ms = 20;
         // The maximum amount of time an app can have its jobs running within a 24 hour window.
         optional int64 max_execution_time_ms = 7;
         // The maximum number of jobs an app can run within this particular standby bucket's
@@ -296,6 +300,9 @@
         // The maximum number of jobs an app can run within this particular standby bucket's
         // window size.
         optional int32 max_job_count_rare = 11;
+        // The maximum number of jobs an app can run within this particular standby bucket's
+        // window size.
+        optional int32 max_job_count_restricted = 21;
         // The period of time used to rate limit recently run jobs.
         optional int32 rate_limiting_window_ms = 19;
         // The maximum number of jobs that should be allowed to run in the past
@@ -313,12 +320,17 @@
         // The maximum number of timing sessions an app can run within this particular standby
         // bucket's window size.
         optional int32 max_session_count_rare = 16;
+        // The maximum number of timing sessions an app can run within this particular standby
+        // bucket's window size.
+        optional int32 max_session_count_restricted = 22;
         // The maximum number of timing sessions that should be allowed to run in the past
         // rate_limiting_window_ms.
         optional int32 max_session_count_per_rate_limiting_window = 17;
         // Treat two distinct {@link TimingSession}s as the same if they start and end within this
         // amount of time of each other.
         optional int64 timing_session_coalescing_duration_ms = 18;
+
+        // Next tag: 23
     }
     optional QuotaController quota_controller = 24;
 
@@ -943,6 +955,9 @@
     optional JobInfo job_info = 6;
 
     repeated ConstraintEnum required_constraints = 7;
+    // Dynamic constraints are additional constraints imposed by the system that MUST be met before
+    // the app can run if the app does not have quota.
+    repeated ConstraintEnum dynamic_constraints = 31;
     repeated ConstraintEnum satisfied_constraints = 8;
     repeated ConstraintEnum unsatisfied_constraints = 9;
     optional bool is_doze_whitelisted = 10;
@@ -956,6 +971,8 @@
         // Battery Saver). This implicit constraint must be satisfied for the
         // job to run.
         optional bool is_not_restricted_in_bg = 2;
+        // True if dynamic constraints have been satisfied.
+        optional bool is_dynamic_satisfied = 3;
     }
     optional ImplicitConstraints implicit_constraints = 25;
 
@@ -998,6 +1015,7 @@
         FREQUENT = 2;
         RARE = 3;
         NEVER = 4;
+        RESTRICTED = 5;
     }
     optional Bucket standby_bucket = 17;
     optional bool is_exempted_from_app_standby = 27;
@@ -1028,7 +1046,7 @@
     // was no attempt.
     optional int64 time_since_first_force_batch_attempt_ms = 29;
 
-    // Next tag: 31
+    // Next tag: 32
 }
 
 // Dump from com.android.server.job.JobConcurrencyManager.