diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index ef1351e6d597a..b96161aba758b 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 class JobParameters implements Parcelable { 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 @@ public class JobParameters implements Parcelable { REASON_TIMEOUT, REASON_DEVICE_IDLE, REASON_DEVICE_THERMAL, + REASON_RESTRAINED, }; /** @@ -80,6 +88,7 @@ public class JobParameters implements Parcelable { 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 102e8485aac52..e0c6d532900e4 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.DeviceIdleJobsController; 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 @@ public class JobSchedulerService extends com.android.server.SystemService /** List of controllers that will notify this service of updates to jobs. */ final List mControllers; + /** + * List of controllers that will apply to all jobs in the RESTRICTED bucket. This is a subset of + * {@link #mControllers}. + */ + private final List mRestrictiveControllers; /** Need direct access to this for testing. */ private final BatteryController mBatteryController; /** Need direct access to this for testing. */ @@ -312,6 +318,9 @@ public class JobSchedulerService extends com.android.server.SystemService 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 @@ public class JobSchedulerService extends com.android.server.SystemService } } + @Override + public void onRestrictedBucketChanged(List 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 @@ public class JobSchedulerService extends com.android.server.SystemService // Create the controllers. mControllers = new ArrayList(); - 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 @@ public class JobSchedulerService extends com.android.server.SystemService 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 @@ public class JobSchedulerService extends com.android.server.SystemService } } 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 class JobSchedulerService extends com.android.server.SystemService 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 87bfc27a715fc..cb3c437141110 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 interface StateChangedListener { 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 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 46658ad33b854..461ef21af7eef 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 @@ import java.util.function.Predicate; * 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); @@ -72,6 +72,11 @@ public final class BatteryController extends StateController { } } + @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)) { @@ -79,6 +84,13 @@ public final class BatteryController extends StateController { } } + @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 8eeea1ba17e29..a0e83daf877dd 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.LINK_BANDWIDTH_UNSPECIFIED; 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 @@ import java.util.function.Predicate; * * 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 @@ public final class ConnectivityController extends StateController implements } } + @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 @@ public final class ConnectivityController extends StateController implements 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 d355715920b65..c0b3204192d67 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 com.android.server.job.controllers.idle.IdlenessTracker; 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 @@ -56,6 +64,11 @@ public final class IdleController extends StateController implements IdlenessLis } } + @Override + public void startTrackingRestrictedJobLocked(JobStatus jobStatus) { + maybeStartTrackingJobLocked(jobStatus, null); + } + @Override public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) { @@ -64,6 +77,13 @@ public final class IdleController extends StateController implements IdlenessLis } } + @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 a8d8bd9b2621b..dbdce70dda036 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 final class JobStatus { 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 @@ public final class JobStatus { /** 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { /** 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { /** 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 @@ public final class JobStatus { } 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { break; default: satisfied |= constraint; + mReadyDynamicSatisfied = + mDynamicConstraints == (satisfied & mDynamicConstraints); break; } @@ -1117,24 +1181,29 @@ public final class JobStatus { 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 @@ public final class JobStatus { case 2: return "FREQUENT"; case 3: return "RARE"; case 4: return "NEVER"; + case 5: + return "RESTRICTED"; default: return "Unknown: " + standbyBucket; } @@ -1560,6 +1631,10 @@ public final class JobStatus { 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 @@ public final class JobStatus { 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 @@ public final class JobStatus { } 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 @@ public final class JobStatus { 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 2e735a4e45f5f..8eefac8abdac7 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.ACTIVE_INDEX; 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { // 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 @@ public final class QuotaController extends StateController { } } + private class TimerChargingUpdateFunctor implements Consumer { + 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 @@ public final class QuotaController extends StateController { } 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 @@ public final class QuotaController extends StateController { Slog.i(TAG, "Moving pkg " + string(userId, packageName) + " to bucketIndex " + bucketIndex); } + List restrictedChanges = new ArrayList<>(); synchronized (mLock) { ArraySet jobs = mTrackedJobs.get(userId, packageName); if (jobs == null || jobs.size() == 0) { @@ -1677,6 +1715,13 @@ public final class QuotaController extends StateController { } 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 @@ public final class QuotaController extends StateController { mStateChangedListener.onControllerStateChanged(); } } + if (restrictedChanges.size() > 0) { + mStateChangedListener.onRestrictedBucketChanged(restrictedChanges); + } }); } } @@ -1863,11 +1911,13 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { (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 @@ public final class QuotaController extends StateController { 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 @@ -1953,6 +2009,13 @@ public final class QuotaController extends StateController { */ 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. */ @@ -1982,6 +2045,12 @@ public final class QuotaController extends StateController { */ 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; @@ -2015,6 +2084,12 @@ public final class QuotaController extends StateController { */ 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 @@ public final class QuotaController extends StateController { 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 0000000000000..5c637bb92ccc6 --- /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 4372e2245ee83..f36bebb9b3aa3 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -810,6 +810,7 @@ message ScheduledJobStateChanged { 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 f702b3e37bda5..d2bf20591f928 100644 --- a/core/proto/android/app/job/enums.proto +++ b/core/proto/android/app/job/enums.proto @@ -34,4 +34,5 @@ enum StopReasonEnum { 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 b71e5395730e3..303d62dbb30a8 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -282,6 +282,10 @@ message ConstantsProto { // 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 @@ message ConstantsProto { // 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 @@ message ConstantsProto { // 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 @@ message JobStatusDumpProto { 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 @@ message JobStatusDumpProto { // 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 @@ message JobStatusDumpProto { 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 @@ message JobStatusDumpProto { // 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.