diff options
10 files changed, 1000 insertions, 23 deletions
diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java index fd8ddbcf3809..6c8af39015f5 100644 --- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -59,6 +59,10 @@ public interface JobSchedulerInternal { */ void reportAppUsage(String packageName, int userId); + /** @return {@code true} if the app is considered buggy from JobScheduler's perspective. */ + boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName, + int timeoutBlameUserId, @NonNull String timeoutBlamePackageName); + /** * @return {@code true} if the given notification is associated with any user-initiated jobs. */ 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 cbc9263a2c3d..f99bcf144b91 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -322,16 +322,25 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = ".schedulePersisted out-of-quota logged"; + private static final String QUOTA_TRACKER_TIMEOUT_UIJ_TAG = "timeout-uij"; + private static final String QUOTA_TRACKER_TIMEOUT_EJ_TAG = "timeout-ej"; + private static final String QUOTA_TRACKER_TIMEOUT_REG_TAG = "timeout-reg"; + private static final String QUOTA_TRACKER_TIMEOUT_TOTAL_TAG = "timeout-total"; + private static final String QUOTA_TRACKER_ANR_TAG = "anr"; private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED = new Category( ".schedulePersisted()"); private static final Category QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED = new Category( ".schedulePersisted out-of-quota logged"); - private static final Categorizer QUOTA_CATEGORIZER = (userId, packageName, tag) -> { - if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) { - return QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED; - } - return QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED; - }; + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ = + new Category(QUOTA_TRACKER_TIMEOUT_UIJ_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ = + new Category(QUOTA_TRACKER_TIMEOUT_EJ_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_REG = + new Category(QUOTA_TRACKER_TIMEOUT_REG_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL = + new Category(QUOTA_TRACKER_TIMEOUT_TOTAL_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_ANR = new Category(QUOTA_TRACKER_ANR_TAG); + private static final Category QUOTA_TRACKER_CATEGORY_DISABLED = new Category("disabled"); /** * Queue of pending jobs. The JobServiceContext class will receive jobs from this list @@ -493,10 +502,18 @@ public class JobSchedulerService extends com.android.server.SystemService } switch (name) { case Constants.KEY_ENABLE_API_QUOTAS: + case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC: case Constants.KEY_API_QUOTA_SCHEDULE_COUNT: case Constants.KEY_API_QUOTA_SCHEDULE_WINDOW_MS: case Constants.KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT: case Constants.KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT: + case Constants.KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS: if (!apiQuotaScheduleUpdated) { mConstants.updateApiQuotaConstantsLocked(); updateQuotaTracker(); @@ -583,10 +600,26 @@ public class JobSchedulerService extends com.android.server.SystemService @VisibleForTesting void updateQuotaTracker() { - mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); + mQuotaTracker.setEnabled( + mConstants.ENABLE_API_QUOTAS || mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_REG, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_ANR, + mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + mConstants.EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS); } /** @@ -616,6 +649,8 @@ public class JobSchedulerService extends com.android.server.SystemService "conn_low_signal_strength_relax_frac"; private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = "prefetch_force_batch_relax_threshold_ms"; + // This has been enabled for 3+ full releases. We're unlikely to disable it. + // TODO(141645789): remove this flag private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas"; private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count"; private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; @@ -623,6 +658,22 @@ public class JobSchedulerService extends com.android.server.SystemService "aq_schedule_throw_exception"; private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; + private static final String KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC = + "enable_execution_safeguards_udc"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = + "es_u_timeout_uij_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = + "es_u_timeout_ej_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = + "es_u_timeout_reg_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = + "es_u_timeout_total_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + "es_u_timeout_window_ms"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = + "es_u_anr_count"; + private static final String KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + "es_u_anr_window_ms"; private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = "runtime_free_quota_max_limit_ms"; @@ -662,6 +713,17 @@ public class JobSchedulerService extends com.android.server.SystemService private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true; private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; + private static final boolean DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + // EJs have a shorter timeout, so set a higher limit for them to start with. + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 5; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 3; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = 10; + private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + 24 * HOUR_IN_MILLIS; + private static final int DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = 3; + private static final long DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + 6 * HOUR_IN_MILLIS; @VisibleForTesting public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS; @VisibleForTesting @@ -774,6 +836,55 @@ public class JobSchedulerService extends com.android.server.SystemService public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; + /** + * Whether to enable the execution safeguards added in UDC. + */ + public boolean ENABLE_EXECUTION_SAFEGUARDS_UDC = DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC; + /** + * The maximum number of times an app can have a user-iniated job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT; + /** + * The maximum number of times an app can have an expedited job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT; + /** + * The maximum number of times an app can have a regular job time out before the system + * begins removing some of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT; + /** + * The maximum number of times an app can have jobs time out before the system + * attempts to restrict most of the app's privileges. + */ + public int EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT; + /** + * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT}, + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT}, + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT}, and + * {@link #EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT} should be evaluated over. + */ + public long EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS; + + /** + * The maximum number of times an app can ANR from JobScheduler's perspective before + * JobScheduler will attempt to restrict the app. + */ + public int EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT; + /** + * The time window that {@link #EXECUTION_SAFEGUARDS_UDC_ANR_COUNT} + * should be evaluated over. + */ + public long EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS; + /** The maximum amount of time we will let a job run for when quota is "free". */ public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; @@ -915,6 +1026,9 @@ public class JobSchedulerService extends com.android.server.SystemService private void updateApiQuotaConstantsLocked() { ENABLE_API_QUOTAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_ENABLE_API_QUOTAS, DEFAULT_ENABLE_API_QUOTAS); + ENABLE_EXECUTION_SAFEGUARDS_UDC = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, DEFAULT_ENABLE_EXECUTION_SAFEGUARDS_UDC); // Set a minimum value on the quota limit so it's not so low that it interferes with // legitimate use cases. API_QUOTA_SCHEDULE_COUNT = Math.max(250, @@ -931,6 +1045,40 @@ public class JobSchedulerService extends com.android.server.SystemService DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); + + // Set a minimum value on the timeout limit so it's not so low that it interferes with + // legitimate use cases. + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = Math.max(2, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT)); + final int highestTimeoutCount = Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + Math.max(EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT = Math.max(highestTimeoutCount, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT)); + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS); + EXECUTION_SAFEGUARDS_UDC_ANR_COUNT = Math.max(1, + DeviceConfig.getInt(DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT)); + EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS = DeviceConfig.getLong( + DeviceConfig.NAMESPACE_JOB_SCHEDULER, + KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS, + DEFAULT_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS); } private void updateRuntimeConstantsLocked() { @@ -1029,6 +1177,23 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); + pw.print(KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC, ENABLE_EXECUTION_SAFEGUARDS_UDC) + .println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_TOTAL_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS, + EXECUTION_SAFEGUARDS_UDC_TIMEOUT_WINDOW_MS).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_COUNT, + EXECUTION_SAFEGUARDS_UDC_ANR_COUNT).println(); + pw.print(KEY_EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS, + EXECUTION_SAFEGUARDS_UDC_ANR_WINDOW_MS).println(); + pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) @@ -2252,12 +2417,52 @@ public class JobSchedulerService extends com.android.server.SystemService // Set up the app standby bucketing tracker mStandbyTracker = new StandbyTracker(); mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class); - mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER); - mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, - mConstants.API_QUOTA_SCHEDULE_COUNT, - mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); + + final Categorizer quotaCategorizer = (userId, packageName, tag) -> { + if (QUOTA_TRACKER_TIMEOUT_UIJ_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_UIJ + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_EJ_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_EJ + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_REG_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_REG + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_TIMEOUT_TOTAL_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_TIMEOUT_TOTAL + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_ANR_TAG.equals(tag)) { + return mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC + ? QUOTA_TRACKER_CATEGORY_ANR + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG.equals(tag)) { + return mConstants.ENABLE_API_QUOTAS + ? QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + if (QUOTA_TRACKER_SCHEDULE_LOGGED.equals(tag)) { + return mConstants.ENABLE_API_QUOTAS + ? QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED + : QUOTA_TRACKER_CATEGORY_DISABLED; + } + Slog.wtf(TAG, "Unexpected category tag: " + tag); + return QUOTA_TRACKER_CATEGORY_DISABLED; + }; + mQuotaTracker = new CountQuotaTracker(context, quotaCategorizer); + updateQuotaTracker(); // Log at most once per minute. + // Set outside updateQuotaTracker() since this is intentionally not configurable. mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000); + mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_DISABLED, Integer.MAX_VALUE, 60_000); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mAppStandbyInternal.addListener(mStandbyTracker); @@ -2762,6 +2967,48 @@ public class JobSchedulerService extends com.android.server.SystemService 0 /* Reset cumulativeExecutionTime because of successful execution */); } + @VisibleForTesting + void maybeProcessBuggyJob(@NonNull JobStatus jobStatus, int debugStopReason) { + boolean jobTimedOut = debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT; + // If madeActive = 0, the job never actually started. + if (!jobTimedOut && jobStatus.madeActive > 0) { + final long executionDurationMs = sUptimeMillisClock.millis() - jobStatus.madeActive; + // The debug reason may be different if we stopped the job for some other reason + // (eg. constraints), so look at total execution time to be safe. + if (jobStatus.startedAsUserInitiatedJob) { + // TODO: factor in different min guarantees for different UI job types + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + } else if (jobStatus.startedAsExpeditedJob) { + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + } else { + jobTimedOut = executionDurationMs >= mConstants.RUNTIME_MIN_GUARANTEE_MS; + } + } + if (jobTimedOut) { + final int userId = jobStatus.getTimeoutBlameUserId(); + final String pkg = jobStatus.getTimeoutBlamePackageName(); + mQuotaTracker.noteEvent(userId, pkg, + jobStatus.startedAsUserInitiatedJob + ? QUOTA_TRACKER_TIMEOUT_UIJ_TAG + : (jobStatus.startedAsExpeditedJob + ? QUOTA_TRACKER_TIMEOUT_EJ_TAG + : QUOTA_TRACKER_TIMEOUT_REG_TAG)); + if (!mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_TIMEOUT_TOTAL_TAG)) { + mAppStandbyInternal.restrictApp( + pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + } + } + + if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_ANR) { + final int callingUserId = jobStatus.getUserId(); + final String callingPkg = jobStatus.getServiceComponent().getPackageName(); + if (!mQuotaTracker.noteEvent(callingUserId, callingPkg, QUOTA_TRACKER_ANR_TAG)) { + mAppStandbyInternal.restrictApp(callingPkg, callingUserId, + UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); + } + } + } + // JobCompletedListener implementations. /** @@ -2784,6 +3031,8 @@ public class JobSchedulerService extends com.android.server.SystemService mLastCompletedJobTimeElapsed[mLastCompletedJobIndex] = sElapsedRealtimeClock.millis(); mLastCompletedJobIndex = (mLastCompletedJobIndex + 1) % NUM_COMPLETED_JOB_HISTORY; + maybeProcessBuggyJob(jobStatus, debugStopReason); + if (debugStopReason == JobParameters.INTERNAL_STOP_REASON_UNINSTALL || debugStopReason == JobParameters.INTERNAL_STOP_REASON_DATA_CLEARED) { // The job should have already been cleared from the rest of the JS tracking. No need @@ -3511,26 +3760,36 @@ public class JobSchedulerService extends com.android.server.SystemService if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( job.getSourceUid(), job.getSourcePackageName())) { + // The calling package is the one doing the work, so use it in the + // timeout quota checks. + final boolean isWithinTimeoutQuota = mQuotaTracker.isWithinQuota( + job.getTimeoutBlameUserId(), job.getTimeoutBlamePackageName(), + QUOTA_TRACKER_TIMEOUT_UIJ_TAG); + final long upperLimitMs = isWithinTimeoutQuota + ? mConstants.RUNTIME_UI_LIMIT_MS + : mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; if (job.getJob().getRequiredNetwork() != null) { // User-initiated data transfers. if (mConstants.RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS) { final long estimatedTransferTimeMs = mConnectivityController.getEstimatedTransferTimeMs(job); if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) { - return mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS; + return Math.min(upperLimitMs, + mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS); } // Try to give the job at least as much time as we think the transfer // will take, but cap it at the maximum limit. final long factoredTransferTimeMs = (long) (estimatedTransferTimeMs * mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR); - return Math.min(mConstants.RUNTIME_UI_LIMIT_MS, + return Math.min(upperLimitMs, Math.max(factoredTransferTimeMs, mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); } - return Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, - mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS); + return Math.min(upperLimitMs, + Math.max(mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mConstants.RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS)); } - return mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + return Math.min(upperLimitMs, mConstants.RUNTIME_MIN_UI_GUARANTEE_MS); } else if (job.shouldTreatAsExpeditedJob()) { // Don't guarantee RESTRICTED jobs more than 5 minutes. return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX @@ -3547,13 +3806,24 @@ public class JobSchedulerService extends com.android.server.SystemService synchronized (mLock) { if (job.shouldTreatAsUserInitiatedJob() && checkRunUserInitiatedJobsPermission( - job.getSourceUid(), job.getSourcePackageName())) { + job.getSourceUid(), job.getSourcePackageName()) + && mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), + job.getTimeoutBlamePackageName(), + QUOTA_TRACKER_TIMEOUT_UIJ_TAG)) { return mConstants.RUNTIME_UI_LIMIT_MS; } if (job.shouldTreatAsUserInitiatedJob()) { return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; } - return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + // Only let the app use the higher runtime if it hasn't repeatedly timed out. + final String timeoutTag = job.shouldTreatAsExpeditedJob() + ? QUOTA_TRACKER_TIMEOUT_EJ_TAG : QUOTA_TRACKER_TIMEOUT_REG_TAG; + final long upperLimitMs = + mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), + job.getTimeoutBlamePackageName(), timeoutTag) + ? mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS + : mConstants.RUNTIME_MIN_GUARANTEE_MS; + return Math.min(upperLimitMs, mConstants.USE_TARE_POLICY ? mTareController.getMaxJobExecutionTimeMsLocked(job) : mQuotaController.getMaxJobExecutionTimeMsLocked(job)); @@ -3797,6 +4067,17 @@ public class JobSchedulerService extends com.android.server.SystemService } @Override + public boolean isAppConsideredBuggy(int callingUserId, @NonNull String callingPackageName, + int timeoutBlameUserId, @NonNull String timeoutBlamePackageName) { + return !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName, + QUOTA_TRACKER_ANR_TAG) + || !mQuotaTracker.isWithinQuota(callingUserId, callingPackageName, + QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG) + || !mQuotaTracker.isWithinQuota(timeoutBlameUserId, timeoutBlamePackageName, + QUOTA_TRACKER_TIMEOUT_TOTAL_TAG); + } + + @Override public boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId, int userId, @NonNull String packageName) { if (packageName == null) { 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 edd531d13965..3baa9e6d3de9 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 @@ -1088,13 +1088,77 @@ public final class JobStatus { return UserHandle.getUserId(callingUid); } + private boolean shouldBlameSourceForTimeout() { + // If the system scheduled the job on behalf of an app, assume the app is the one + // doing the work and blame the app directly. This is the case with things like + // syncs via SyncManager. + // If the system didn't schedule the job on behalf of an app, then + // blame the app doing the actual work. Proxied jobs are a little tricky. + // Proxied jobs scheduled by built-in system apps like DownloadManager may be fine + // and we could consider exempting those jobs. For example, in DownloadManager's + // case, all it does is download files and the code is vetted. A timeout likely + // means it's downloading a large file, which isn't an error. For now, DownloadManager + // is an exempted app, so this shouldn't be an issue. + // However, proxied jobs coming from other system apps (such as those that can + // be updated separately from an OTA) may not be fine and we would want to apply + // this policy to those jobs/apps. + // TODO(284512488): consider exempting DownloadManager or other system apps + return UserHandle.isCore(callingUid); + } + + /** + * Returns the package name that should most likely be blamed for the job timing out. + */ + public String getTimeoutBlamePackageName() { + if (shouldBlameSourceForTimeout()) { + return sourcePackageName; + } + return getServiceComponent().getPackageName(); + } + + /** + * Returns the UID that should most likely be blamed for the job timing out. + */ + public int getTimeoutBlameUid() { + if (shouldBlameSourceForTimeout()) { + return sourceUid; + } + return callingUid; + } + + /** + * Returns the userId that should most likely be blamed for the job timing out. + */ + public int getTimeoutBlameUserId() { + if (shouldBlameSourceForTimeout()) { + return sourceUserId; + } + return UserHandle.getUserId(callingUid); + } + /** * Returns an appropriate standby bucket for the job, taking into account any standby * exemptions. */ public int getEffectiveStandbyBucket() { + final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class); + final boolean isBuggy = jsi.isAppConsideredBuggy( + getUserId(), getServiceComponent().getPackageName(), + getTimeoutBlameUserId(), getTimeoutBlamePackageName()); + final int actualBucket = getStandbyBucket(); if (actualBucket == EXEMPTED_INDEX) { + // EXEMPTED apps always have their jobs exempted, even if they're buggy, because the + // user has explicitly told the system to avoid restricting the app for power reasons. + if (isBuggy) { + final String pkg; + if (getServiceComponent().getPackageName().equals(sourcePackageName)) { + pkg = sourcePackageName; + } else { + pkg = getServiceComponent().getPackageName() + "/" + sourcePackageName; + } + Slog.w(TAG, "Exempted app " + pkg + " considered buggy"); + } return actualBucket; } if (uidActive || getJob().isExemptedFromAppStandby()) { @@ -1102,13 +1166,18 @@ public final class JobStatus { // like other ACTIVE apps. return ACTIVE_INDEX; } + // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket + // (potentially because it's used frequently by the user), limit its effective bucket + // so that it doesn't get to run as much as a normal ACTIVE app. + final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX; if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && mHasMediaBackupExemption) { - // Cap it at WORKING_INDEX as media back up jobs are important to the user, and the + // Treat it as if it's at least WORKING_INDEX since media backup jobs are important + // to the user, and the // source package may not have been used directly in a while. - return Math.min(WORKING_INDEX, actualBucket); + return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket)); } - return actualBucket; + return Math.max(highestBucket, actualBucket); } /** Returns the real standby bucket of the job. */ 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 07958dd0fef5..1c29982dbd48 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 @@ -773,6 +773,14 @@ public final class QuotaController extends StateController { // If quota is currently "free", then the job can run for the full amount of time, // regardless of bucket (hence using charging instead of isQuotaFreeLocked()). if (mService.isBatteryCharging() + // The top and foreground cases here were added because apps in those states + // aren't really restricted and the work could be something the user is + // waiting for. Now that user-initiated jobs are a defined concept, we may + // not need these exemptions as much. However, UIJs are currently limited + // (as of UDC) to data transfer work. There may be other work that could + // rely on this exception. Once we add more UIJ types, we can re-evaluate + // the need for these exceptions. + // TODO: re-evaluate the need for these exceptions || mTopAppCache.get(jobStatus.getSourceUid()) || isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) { diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 55e681521048..7d3837786be9 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -3025,7 +3025,7 @@ public class AppStandbyController public static final long DEFAULT_INITIAL_FOREGROUND_SERVICE_START_TIMEOUT = COMPRESS_TIME ? ONE_MINUTE : 30 * ONE_MINUTE; public static final long DEFAULT_AUTO_RESTRICTED_BUCKET_DELAY_MS = - COMPRESS_TIME ? ONE_MINUTE : ONE_DAY; + COMPRESS_TIME ? ONE_MINUTE : ONE_HOUR; public static final boolean DEFAULT_CROSS_PROFILE_APPS_SHARE_STANDBY_BUCKETS = true; public static final long DEFAULT_BROADCAST_RESPONSE_WINDOW_DURATION_MS = 2 * ONE_MINUTE; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index c5ff8cc7b0d6..dd23d9f006e3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -28,6 +28,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.JobSchedulerService.sUptimeMillisClock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -148,6 +149,9 @@ public class JobSchedulerServiceTest { // Used in JobConcurrencyManager. doReturn(mock(UserManagerInternal.class)) .when(() -> LocalServices.getService(UserManagerInternal.class)); + // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); // Called via IdleController constructor. when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); @@ -168,6 +172,8 @@ public class JobSchedulerServiceTest { JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + // Make sure the uptime is at least 24 hours so that tests that rely on high uptime work. + sUptimeMillisClock = getAdvancedClock(sUptimeMillisClock, 24 * HOUR_IN_MILLIS); // Called by DeviceIdlenessTracker when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); @@ -313,6 +319,260 @@ public class JobSchedulerServiceTest { } @Test + public void testGetMinJobExecutionGuaranteeMs_timeoutSafeguards_disabled() { + JobStatus jobUij = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = false; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // Safeguards disabled -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 UIJ timeouts. Safeguards disabled -> no penalties. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 EJ timeouts. Safeguards disabled -> no penalties. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 Reg timeouts. Safeguards disabled -> no penalties. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + } + + @Test + public void testGetMinJobExecutionGuaranteeMs_timeoutSafeguards_enabled() { + JobStatus jobUij = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMinJobExecutionGuaranteeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // No timeouts -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 UIJ timeout. No execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // Not a timeout -> 1 UIJ timeout. No execution penalty yet. + jobUij.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 UIJ timeouts. Min execution penalty only for UIJs. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 EJ timeouts. Max execution penalty for EJs. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + + // 2 Reg timeouts. Max execution penalty for regular jobs. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobReg)); + } + + @Test public void testGetMaxJobExecutionTimeMs() { JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs", createJobInfo(10) @@ -327,7 +587,7 @@ public class JobSchedulerServiceTest { doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) - .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); grantRunUserInitiatedJobsPermission(true); assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, @@ -337,6 +597,306 @@ public class JobSchedulerServiceTest { mService.getMaxJobExecutionTimeMs(jobUIDT)); } + @Test + public void testGetMaxJobExecutionTimeMs_timeoutSafeguards_disabled() { + JobStatus jobUij = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + QuotaController quotaController = mService.getQuotaController(); + spyOn(quotaController); + TareController tareController = mService.getTareController(); + spyOn(tareController); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = false; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // Safeguards disabled -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 UIJ timeouts. Safeguards disabled -> no penalties. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 EJ timeouts. Safeguards disabled -> no penalties. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 Reg timeouts. Safeguards disabled -> no penalties. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + } + + @Test + public void testGetMaxJobExecutionTimeMs_timeoutSafeguards_enabled() { + JobStatus jobUij = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(1) + .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs_timeoutSafeguards", + createJobInfo(3)); + spyOn(jobUij); + when(jobUij.shouldTreatAsUserInitiatedJob()).thenReturn(true); + jobUij.startedAsUserInitiatedJob = true; + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); + jobEj.startedAsExpeditedJob = true; + + QuotaController quotaController = mService.getQuotaController(); + spyOn(quotaController); + TareController tareController = mService.getTareController(); + spyOn(tareController); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(quotaController).getMaxJobExecutionTimeMsLocked(any()); + doReturn(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) + .when(tareController).getMaxJobExecutionTimeMsLocked(any()); + + mService.mConstants.ENABLE_EXECUTION_SAFEGUARDS_UDC = true; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_UIJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_EJ_COUNT = 2; + mService.mConstants.EXECUTION_SAFEGUARDS_UDC_TIMEOUT_REG_COUNT = 2; + mService.updateQuotaTracker(); + mService.resetScheduleQuota(); + + // No timeouts -> no penalties. + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 UIJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 UIJ timeout. No max execution penalty yet. + jobUij.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_UI_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 UIJ timeouts. Max execution penalty only for UIJs. + jobUij.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_UI_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobUij, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 EJ timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 EJ timeout. No max execution penalty yet. + jobEj.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 EJ timeouts. Max execution penalty for EJs. + jobEj.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobEj, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 1 reg timeout. No max execution penalty yet. + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // Not a timeout -> 1 reg timeout. No max execution penalty yet. + jobReg.madeActive = sUptimeMillisClock.millis() - 1; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + + // 2 Reg timeouts. Max execution penalty for regular jobs. + jobReg.madeActive = + sUptimeMillisClock.millis() - mService.mConstants.RUNTIME_MIN_GUARANTEE_MS; + mService.maybeProcessBuggyJob(jobReg, JobParameters.INTERNAL_STOP_REASON_UNKNOWN); + grantRunUserInitiatedJobsPermission(true); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + grantRunUserInitiatedJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUij)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); + } + /** * Confirm that * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)} @@ -1226,6 +1786,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1249,6 +1810,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1270,6 +1832,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1292,6 +1855,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { @@ -1315,6 +1879,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(false).build(); final JobWorkItem item = new JobWorkItem.Builder().build(); @@ -1337,6 +1902,7 @@ public class JobSchedulerServiceTest { mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); + mService.resetScheduleQuota(); final JobInfo job = createJobInfo().setPersisted(true).build(); final JobWorkItem item = new JobWorkItem.Builder().build(); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 2180a781e437..2b56ea8bba33 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -73,6 +73,7 @@ import android.telephony.TelephonyManager; import android.util.DataUnit; import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobSchedulerService.Constants; import com.android.server.net.NetworkPolicyManagerInternal; @@ -124,6 +125,10 @@ public class ConnectivityControllerTest { LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class); LocalServices.addService(NetworkPolicyManagerInternal.class, mNetPolicyManagerInternal); + // Used in JobStatus. + LocalServices.removeServiceForTest(JobSchedulerInternal.class); + LocalServices.addService(JobSchedulerInternal.class, mock(JobSchedulerInternal.class)); + // Freeze the clocks at this moment in time JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 05780ebe6c4b..1de7e3719112 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -21,6 +21,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.EXEMPTED_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; @@ -45,6 +46,8 @@ import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.when; @@ -582,6 +585,40 @@ public class JobStatusTest { } @Test + public void testGetEffectiveStandbyBucket_buggyApp() { + when(mJobSchedulerInternal.isAppConsideredBuggy( + anyInt(), anyString(), anyInt(), anyString())) + .thenReturn(true); + + final JobInfo jobInfo = new JobInfo.Builder(1234, TEST_JOB_COMPONENT).build(); + JobStatus job = createJobStatus(jobInfo); + + // Exempt apps be exempting. + job.setStandbyBucket(EXEMPTED_INDEX); + assertEquals(EXEMPTED_INDEX, job.getEffectiveStandbyBucket()); + + // Actual bucket is higher than the buggy cap, so the cap comes into effect. + job.setStandbyBucket(ACTIVE_INDEX); + assertEquals(WORKING_INDEX, job.getEffectiveStandbyBucket()); + + // Buckets at the cap or below shouldn't be affected. + job.setStandbyBucket(WORKING_INDEX); + assertEquals(WORKING_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(FREQUENT_INDEX); + assertEquals(FREQUENT_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(RARE_INDEX); + assertEquals(RARE_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(RESTRICTED_INDEX); + assertEquals(RESTRICTED_INDEX, job.getEffectiveStandbyBucket()); + + job.setStandbyBucket(NEVER_INDEX); + assertEquals(NEVER_INDEX, job.getEffectiveStandbyBucket()); + } + + @Test public void testModifyingInternalFlags() { final JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java index fb59ea2bb63b..7cc01e1b4292 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java @@ -58,6 +58,7 @@ import android.util.SparseArray; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.controllers.PrefetchController.PcConstants; @@ -135,6 +136,9 @@ public class PrefetchControllerTest { when(mJobSchedulerService.getPackagesForUidLocked(anyInt())) .thenAnswer(invocationOnMock -> mPackagesForUid.get(invocationOnMock.getArgument(0))); + // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions // in the past, and PrefetchController sometimes floors values at 0, so if the test time diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 6f713e0fad64..dce162c58d0b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -85,6 +85,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; +import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobStore; import com.android.server.job.controllers.QuotaController.ExecutionStats; @@ -190,6 +191,8 @@ public class QuotaControllerTest { doReturn(mPowerAllowlistInternal) .when(() -> LocalServices.getService(PowerAllowlistInternal.class)); // Used in JobStatus. + doReturn(mock(JobSchedulerInternal.class)) + .when(() -> LocalServices.getService(JobSchedulerInternal.class)); doReturn(mPackageManagerInternal) .when(() -> LocalServices.getService(PackageManagerInternal.class)); // Used in QuotaController.Handler. |