Loading apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +18 −6 Original line number Diff line number Diff line Loading @@ -59,9 +59,9 @@ import java.util.List; * * <p class="caution"><strong>Note:</strong> Beginning with API 30 * ({@link android.os.Build.VERSION_CODES#R}), JobScheduler will throttle runaway applications. * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency is indicative * of an app bug and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin * to throttle apps that show buggy behavior, regardless of target SDK version. * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency can have a * high cost and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin * to throttle apps, regardless of target SDK version. */ @SystemService(Context.JOB_SCHEDULER_SERVICE) public abstract class JobScheduler { Loading @@ -74,9 +74,16 @@ public abstract class JobScheduler { public @interface Result {} /** * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can occur * if the run-time for your job is too short, or perhaps the system can't resolve the * requisite {@link JobService} in your package. * Returned from {@link #schedule(JobInfo)} if a job wasn't scheduled successfully. Scheduling * can fail for a variety of reasons, including, but not limited to: * <ul> * <li>an invalid parameter was supplied (eg. the run-time for your job is too short, or the * system can't resolve the requisite {@link JobService} in your package)</li> * <li>the app has too many jobs scheduled</li> * <li>the app has tried to schedule too many jobs in a short amount of time</li> * </ul> * Attempting to schedule the job again immediately after receiving this result will not * guarantee a successful schedule. */ public static final int RESULT_FAILURE = 0; /** Loading @@ -89,6 +96,11 @@ public abstract class JobScheduler { * ID with the new information in the {@link JobInfo}. If a job with the given ID is currently * running, it will be stopped. * * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's * rescheduling the same job and the job didn't execute, especially on platform versions before * version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to * this API if calls are made too frequently in a short amount of time. * * @param job The job you wish scheduled. See * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs * you can schedule. Loading apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +57 −13 Original line number Diff line number Diff line Loading @@ -254,6 +254,18 @@ public class JobSchedulerService extends com.android.server.SystemService private final CountQuotaTracker mQuotaTracker; 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 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; }; /** * Queue of pending jobs. The JobServiceContext class will receive jobs from this list Loading @@ -270,6 +282,7 @@ public class JobSchedulerService extends com.android.server.SystemService ActivityManagerInternal mActivityManagerInternal; IBatteryStats mBatteryStats; DeviceIdleInternal mLocalDeviceIdleController; @VisibleForTesting AppStateTracker mAppStateTracker; final UsageStatsManagerInternal mUsageStats; private final AppStandbyInternal mAppStandbyInternal; Loading Loading @@ -342,10 +355,7 @@ public class JobSchedulerService extends com.android.server.SystemService final StateController sc = mControllers.get(controller); sc.onConstantsUpdatedLocked(); } mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); updateQuotaTracker(); } catch (IllegalArgumentException e) { // Failed to parse the settings string, log this and move on // with defaults. Loading @@ -355,6 +365,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } @VisibleForTesting void updateQuotaTracker() { mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); } static class MaxJobCounts { private final KeyValueListParser.IntValue mTotal; private final KeyValueListParser.IntValue mMaxBg; Loading Loading @@ -507,6 +525,8 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; private static final String KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION = "aq_schedule_throw_exception"; private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; Loading @@ -520,6 +540,7 @@ public class JobSchedulerService extends com.android.server.SystemService private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250; 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; /** * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. Loading Loading @@ -623,6 +644,11 @@ public class JobSchedulerService extends com.android.server.SystemService */ public boolean API_QUOTA_SCHEDULE_THROW_EXCEPTION = DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION; /** * Whether or not to return a failure result when an app hits its schedule quota limit. */ public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; private final KeyValueListParser mParser = new KeyValueListParser(','); Loading Loading @@ -678,6 +704,9 @@ public class JobSchedulerService extends com.android.server.SystemService API_QUOTA_SCHEDULE_THROW_EXCEPTION = mParser.getBoolean( KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION, DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION); API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = mParser.getBoolean( KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } void dump(IndentingPrintWriter pw) { Loading Loading @@ -712,6 +741,8 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS).println(); pw.print(KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION, API_QUOTA_SCHEDULE_THROW_EXCEPTION).println(); pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); pw.decreaseIndent(); } Loading Loading @@ -740,6 +771,8 @@ public class JobSchedulerService extends com.android.server.SystemService proto.write(ConstantsProto.API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS); proto.write(ConstantsProto.API_QUOTA_SCHEDULE_THROW_EXCEPTION, API_QUOTA_SCHEDULE_THROW_EXCEPTION); proto.write(ConstantsProto.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } } Loading Loading @@ -973,12 +1006,17 @@ public class JobSchedulerService extends com.android.server.SystemService public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName, int userId, String tag) { if (job.isPersisted()) { // Only limit schedule calls for persisted jobs. final String servicePkg = job.getService().getPackageName(); if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) { // Only limit schedule calls for persisted jobs scheduled by the app itself. final String pkg = packageName == null ? job.getService().getPackageName() : packageName; if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) { Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times"); if (mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED)) { // Don't log too frequently Slog.wtf(TAG, userId + "-" + pkg + " has called schedule() too many times"); mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED); } mAppStandbyInternal.restrictApp( pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION) { Loading @@ -1004,14 +1042,18 @@ public class JobSchedulerService extends com.android.server.SystemService // Only throw the exception for debuggable apps. throw new LimitExceededException( "schedule()/enqueue() called more than " + mQuotaTracker.getLimit(Category.SINGLE_CATEGORY) + mQuotaTracker.getLimit( QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED) + " times in the past " + mQuotaTracker.getWindowSizeMs(Category.SINGLE_CATEGORY) + "ms"); + mQuotaTracker.getWindowSizeMs( QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED) + "ms. See the documentation for more information."); } } if (mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT) { return JobScheduler.RESULT_FAILURE; } } mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG); } Loading Loading @@ -1371,10 +1413,12 @@ 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, Categorizer.SINGLE_CATEGORIZER); mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY, mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); // Log at most once per minute. mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mAppStandbyInternal.addListener(mStandbyTracker); Loading core/proto/android/server/jobscheduler.proto +3 −1 Original line number Diff line number Diff line Loading @@ -236,6 +236,8 @@ message ConstantsProto { optional int64 api_quota_schedule_window_ms = 33; // Whether or not to throw an exception when an app hits its schedule quota limit. optional bool api_quota_schedule_throw_exception = 34; // Whether or not to return a failure result when an app hits its schedule quota limit. optional bool api_quota_schedule_return_failure_result = 35; message QuotaController { option (.android.msg_privacy).dest = DEST_AUTOMATIC; Loading Loading @@ -335,7 +337,7 @@ message ConstantsProto { // In this time after screen turns on, we increase job concurrency. optional int32 screen_off_job_concurrency_increase_delay_ms = 28; // Next tag: 35 // Next tag: 36 } // Next tag: 4 Loading services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +92 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.IActivityManager; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Context; Loading @@ -56,6 +57,7 @@ import android.os.SystemClock; import com.android.server.AppStateTracker; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; import com.android.server.SystemServiceManager; import com.android.server.job.controllers.JobStatus; import com.android.server.usage.AppStandbyInternal; Loading @@ -82,6 +84,7 @@ public class JobSchedulerServiceTest { private class TestJobSchedulerService extends JobSchedulerService { TestJobSchedulerService(Context context) { super(context); mAppStateTracker = mock(AppStateTracker.class); } @Override Loading Loading @@ -136,6 +139,9 @@ public class JobSchedulerServiceTest { } catch (RemoteException e) { fail("registerUidObserver threw exception: " + e.getMessage()); } // Called by QuotaTracker doReturn(mock(SystemServiceManager.class)) .when(() -> LocalServices.getService(SystemServiceManager.class)); JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Loading Loading @@ -750,4 +756,90 @@ public class JobSchedulerServiceTest { maybeQueueFunctor.postProcess(); assertEquals(3, mService.mPendingJobs.size()); } /** Tests that jobs scheduled by the app itself are counted towards scheduling limits. */ @Test public void testScheduleLimiting_RegularSchedule_Blocked() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { final int expected = i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE; assertEquals("Got unexpected result for schedule #" + (i + 1), expected, mService.scheduleAsPackage(job, null, 10123, null, 0, "")); } } /** * Tests that jobs scheduled by the app itself succeed even if the app is above the scheduling * limit. */ @Test public void testScheduleLimiting_RegularSchedule_Allowed() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { assertEquals("Got unexpected result for schedule #" + (i + 1), JobScheduler.RESULT_SUCCESS, mService.scheduleAsPackage(job, null, 10123, null, 0, "")); } } /** * Tests that jobs scheduled through a proxy (eg. system server) don't count towards scheduling * limits. */ @Test public void testScheduleLimiting_Proxy() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { assertEquals("Got unexpected result for schedule #" + (i + 1), JobScheduler.RESULT_SUCCESS, mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, "")); } } /** * Tests that jobs scheduled by an app for itself as if through a proxy are counted towards * scheduling limits. */ @Test public void testScheduleLimiting_SelfProxy() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { final int expected = i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE; assertEquals("Got unexpected result for schedule #" + (i + 1), expected, mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(), 0, "")); } } } Loading
apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +18 −6 Original line number Diff line number Diff line Loading @@ -59,9 +59,9 @@ import java.util.List; * * <p class="caution"><strong>Note:</strong> Beginning with API 30 * ({@link android.os.Build.VERSION_CODES#R}), JobScheduler will throttle runaway applications. * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency is indicative * of an app bug and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin * to throttle apps that show buggy behavior, regardless of target SDK version. * Calling {@link #schedule(JobInfo)} and other such methods with very high frequency can have a * high cost and so, to make sure the system doesn't get overwhelmed, JobScheduler will begin * to throttle apps, regardless of target SDK version. */ @SystemService(Context.JOB_SCHEDULER_SERVICE) public abstract class JobScheduler { Loading @@ -74,9 +74,16 @@ public abstract class JobScheduler { public @interface Result {} /** * Returned from {@link #schedule(JobInfo)} when an invalid parameter was supplied. This can occur * if the run-time for your job is too short, or perhaps the system can't resolve the * requisite {@link JobService} in your package. * Returned from {@link #schedule(JobInfo)} if a job wasn't scheduled successfully. Scheduling * can fail for a variety of reasons, including, but not limited to: * <ul> * <li>an invalid parameter was supplied (eg. the run-time for your job is too short, or the * system can't resolve the requisite {@link JobService} in your package)</li> * <li>the app has too many jobs scheduled</li> * <li>the app has tried to schedule too many jobs in a short amount of time</li> * </ul> * Attempting to schedule the job again immediately after receiving this result will not * guarantee a successful schedule. */ public static final int RESULT_FAILURE = 0; /** Loading @@ -89,6 +96,11 @@ public abstract class JobScheduler { * ID with the new information in the {@link JobInfo}. If a job with the given ID is currently * running, it will be stopped. * * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's * rescheduling the same job and the job didn't execute, especially on platform versions before * version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to * this API if calls are made too frequently in a short amount of time. * * @param job The job you wish scheduled. See * {@link android.app.job.JobInfo.Builder JobInfo.Builder} for more detail on the sorts of jobs * you can schedule. Loading
apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +57 −13 Original line number Diff line number Diff line Loading @@ -254,6 +254,18 @@ public class JobSchedulerService extends com.android.server.SystemService private final CountQuotaTracker mQuotaTracker; 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 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; }; /** * Queue of pending jobs. The JobServiceContext class will receive jobs from this list Loading @@ -270,6 +282,7 @@ public class JobSchedulerService extends com.android.server.SystemService ActivityManagerInternal mActivityManagerInternal; IBatteryStats mBatteryStats; DeviceIdleInternal mLocalDeviceIdleController; @VisibleForTesting AppStateTracker mAppStateTracker; final UsageStatsManagerInternal mUsageStats; private final AppStandbyInternal mAppStandbyInternal; Loading Loading @@ -342,10 +355,7 @@ public class JobSchedulerService extends com.android.server.SystemService final StateController sc = mControllers.get(controller); sc.onConstantsUpdatedLocked(); } mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); updateQuotaTracker(); } catch (IllegalArgumentException e) { // Failed to parse the settings string, log this and move on // with defaults. Loading @@ -355,6 +365,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } @VisibleForTesting void updateQuotaTracker() { mQuotaTracker.setEnabled(mConstants.ENABLE_API_QUOTAS); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); } static class MaxJobCounts { private final KeyValueListParser.IntValue mTotal; private final KeyValueListParser.IntValue mMaxBg; Loading Loading @@ -507,6 +525,8 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms"; private static final String KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION = "aq_schedule_throw_exception"; private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = "aq_schedule_return_failure"; private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5; private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS; Loading @@ -520,6 +540,7 @@ public class JobSchedulerService extends com.android.server.SystemService private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250; 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; /** * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early. Loading Loading @@ -623,6 +644,11 @@ public class JobSchedulerService extends com.android.server.SystemService */ public boolean API_QUOTA_SCHEDULE_THROW_EXCEPTION = DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION; /** * Whether or not to return a failure result when an app hits its schedule quota limit. */ public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT; private final KeyValueListParser mParser = new KeyValueListParser(','); Loading Loading @@ -678,6 +704,9 @@ public class JobSchedulerService extends com.android.server.SystemService API_QUOTA_SCHEDULE_THROW_EXCEPTION = mParser.getBoolean( KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION, DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION); API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = mParser.getBoolean( KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } void dump(IndentingPrintWriter pw) { Loading Loading @@ -712,6 +741,8 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(KEY_API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS).println(); pw.print(KEY_API_QUOTA_SCHEDULE_THROW_EXCEPTION, API_QUOTA_SCHEDULE_THROW_EXCEPTION).println(); pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println(); pw.decreaseIndent(); } Loading Loading @@ -740,6 +771,8 @@ public class JobSchedulerService extends com.android.server.SystemService proto.write(ConstantsProto.API_QUOTA_SCHEDULE_WINDOW_MS, API_QUOTA_SCHEDULE_WINDOW_MS); proto.write(ConstantsProto.API_QUOTA_SCHEDULE_THROW_EXCEPTION, API_QUOTA_SCHEDULE_THROW_EXCEPTION); proto.write(ConstantsProto.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT, API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT); } } Loading Loading @@ -973,12 +1006,17 @@ public class JobSchedulerService extends com.android.server.SystemService public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName, int userId, String tag) { if (job.isPersisted()) { // Only limit schedule calls for persisted jobs. final String servicePkg = job.getService().getPackageName(); if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) { // Only limit schedule calls for persisted jobs scheduled by the app itself. final String pkg = packageName == null ? job.getService().getPackageName() : packageName; if (!mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG)) { Slog.e(TAG, userId + "-" + pkg + " has called schedule() too many times"); if (mQuotaTracker.isWithinQuota(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED)) { // Don't log too frequently Slog.wtf(TAG, userId + "-" + pkg + " has called schedule() too many times"); mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_LOGGED); } mAppStandbyInternal.restrictApp( pkg, userId, UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY); if (mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION) { Loading @@ -1004,14 +1042,18 @@ public class JobSchedulerService extends com.android.server.SystemService // Only throw the exception for debuggable apps. throw new LimitExceededException( "schedule()/enqueue() called more than " + mQuotaTracker.getLimit(Category.SINGLE_CATEGORY) + mQuotaTracker.getLimit( QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED) + " times in the past " + mQuotaTracker.getWindowSizeMs(Category.SINGLE_CATEGORY) + "ms"); + mQuotaTracker.getWindowSizeMs( QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED) + "ms. See the documentation for more information."); } } if (mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT) { return JobScheduler.RESULT_FAILURE; } } mQuotaTracker.noteEvent(userId, pkg, QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG); } Loading Loading @@ -1371,10 +1413,12 @@ 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, Categorizer.SINGLE_CATEGORIZER); mQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY, mQuotaTracker = new CountQuotaTracker(context, QUOTA_CATEGORIZER); mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_PERSISTED, mConstants.API_QUOTA_SCHEDULE_COUNT, mConstants.API_QUOTA_SCHEDULE_WINDOW_MS); // Log at most once per minute. mQuotaTracker.setCountLimit(QUOTA_TRACKER_CATEGORY_SCHEDULE_LOGGED, 1, 60_000); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mAppStandbyInternal.addListener(mStandbyTracker); Loading
core/proto/android/server/jobscheduler.proto +3 −1 Original line number Diff line number Diff line Loading @@ -236,6 +236,8 @@ message ConstantsProto { optional int64 api_quota_schedule_window_ms = 33; // Whether or not to throw an exception when an app hits its schedule quota limit. optional bool api_quota_schedule_throw_exception = 34; // Whether or not to return a failure result when an app hits its schedule quota limit. optional bool api_quota_schedule_return_failure_result = 35; message QuotaController { option (.android.msg_privacy).dest = DEST_AUTOMATIC; Loading Loading @@ -335,7 +337,7 @@ message ConstantsProto { // In this time after screen turns on, we increase job concurrency. optional int32 screen_off_job_concurrency_increase_delay_ms = 28; // Next tag: 35 // Next tag: 36 } // Next tag: 4 Loading
services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +92 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.IActivityManager; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Context; Loading @@ -56,6 +57,7 @@ import android.os.SystemClock; import com.android.server.AppStateTracker; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; import com.android.server.SystemServiceManager; import com.android.server.job.controllers.JobStatus; import com.android.server.usage.AppStandbyInternal; Loading @@ -82,6 +84,7 @@ public class JobSchedulerServiceTest { private class TestJobSchedulerService extends JobSchedulerService { TestJobSchedulerService(Context context) { super(context); mAppStateTracker = mock(AppStateTracker.class); } @Override Loading Loading @@ -136,6 +139,9 @@ public class JobSchedulerServiceTest { } catch (RemoteException e) { fail("registerUidObserver threw exception: " + e.getMessage()); } // Called by QuotaTracker doReturn(mock(SystemServiceManager.class)) .when(() -> LocalServices.getService(SystemServiceManager.class)); JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Loading Loading @@ -750,4 +756,90 @@ public class JobSchedulerServiceTest { maybeQueueFunctor.postProcess(); assertEquals(3, mService.mPendingJobs.size()); } /** Tests that jobs scheduled by the app itself are counted towards scheduling limits. */ @Test public void testScheduleLimiting_RegularSchedule_Blocked() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { final int expected = i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE; assertEquals("Got unexpected result for schedule #" + (i + 1), expected, mService.scheduleAsPackage(job, null, 10123, null, 0, "")); } } /** * Tests that jobs scheduled by the app itself succeed even if the app is above the scheduling * limit. */ @Test public void testScheduleLimiting_RegularSchedule_Allowed() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { assertEquals("Got unexpected result for schedule #" + (i + 1), JobScheduler.RESULT_SUCCESS, mService.scheduleAsPackage(job, null, 10123, null, 0, "")); } } /** * Tests that jobs scheduled through a proxy (eg. system server) don't count towards scheduling * limits. */ @Test public void testScheduleLimiting_Proxy() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { assertEquals("Got unexpected result for schedule #" + (i + 1), JobScheduler.RESULT_SUCCESS, mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, "")); } } /** * Tests that jobs scheduled by an app for itself as if through a proxy are counted towards * scheduling limits. */ @Test public void testScheduleLimiting_SelfProxy() { mService.mConstants.ENABLE_API_QUOTAS = true; mService.mConstants.API_QUOTA_SCHEDULE_COUNT = 300; mService.mConstants.API_QUOTA_SCHEDULE_WINDOW_MS = 300000; mService.mConstants.API_QUOTA_SCHEDULE_THROW_EXCEPTION = false; mService.mConstants.API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = true; mService.updateQuotaTracker(); final JobInfo job = createJobInfo().setPersisted(true).build(); for (int i = 0; i < 500; ++i) { final int expected = i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE; assertEquals("Got unexpected result for schedule #" + (i + 1), expected, mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(), 0, "")); } } }