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 ad6eff07b9779..e0df22c5e31d8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -266,6 +266,8 @@ public class JobSchedulerService extends com.android.server.SystemService private final List mRestrictiveControllers; /** Need direct access to this for testing. */ private final StorageController mStorageController; + /** Needed to get estimated transfer time. */ + private final ConnectivityController mConnectivityController; /** Need directly for sending uid state changes */ private final DeviceIdleJobsController mDeviceIdleJobsController; /** Needed to get next estimated launch time. */ @@ -464,6 +466,13 @@ public class JobSchedulerService extends com.android.server.SystemService case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS: case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS: case Constants.KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS: + case Constants.KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS: + case Constants.KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS: + case Constants.KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS: + case Constants.KEY_RUNTIME_USER_INITIATED_LIMIT_MS: + case Constants.KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR: + case Constants.KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS: + case Constants.KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS: if (!runtimeUpdated) { mConstants.updateRuntimeConstantsLocked(); runtimeUpdated = true; @@ -555,6 +564,21 @@ public class JobSchedulerService extends com.android.server.SystemService private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms"; private static final String KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = "runtime_min_high_priority_guarantee_ms"; + private static final String KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = + "runtime_min_data_transfer_guarantee_ms"; + private static final String KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS = + "runtime_data_transfer_limit_ms"; + private static final String KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS = + "runtime_min_user_initiated_guarantee_ms"; + private static final String KEY_RUNTIME_USER_INITIATED_LIMIT_MS = + "runtime_user_initiated_limit_ms"; + private static final String + KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = + "runtime_min_user_initiated_data_transfer_guarantee_buffer_factor"; + private static final String KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = + "runtime_min_user_initiated_data_transfer_guarantee_ms"; + private static final String KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = + "runtime_user_initiated_data_transfer_limit_ms"; private static final String KEY_PERSIST_IN_SPLIT_FILES = "persist_in_split_files"; @@ -584,6 +608,20 @@ public class JobSchedulerService extends com.android.server.SystemService public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS; @VisibleForTesting static final long DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = 5 * MINUTE_IN_MILLIS; + public static final long DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = + DEFAULT_RUNTIME_MIN_GUARANTEE_MS; + public static final long DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS = + DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; + public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS = + Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS); + public static final long DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS = + Math.max(60 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS); + public static final float + DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.35f; + public static final long DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = + Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS); + public static final long DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = + Math.max(Long.MAX_VALUE, DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS); static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true; private static final boolean DEFAULT_USE_TARE_POLICY = false; @@ -699,6 +737,49 @@ public class JobSchedulerService extends com.android.server.SystemService public long RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS; + /** + * The minimum amount of time we try to guarantee normal data transfer jobs will run for. + */ + public long RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = + DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS; + + /** + * The maximum amount of time we will let a normal data transfer job run for. This will only + * apply if there are no other limits that apply to the specific data transfer job. + */ + public long RUNTIME_DATA_TRANSFER_LIMIT_MS = DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS; + + /** + * The minimum amount of time we try to guarantee normal user-initiated jobs will run for. + */ + public long RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS = + DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS; + + /** + * The maximum amount of time we will let a user-initiated job run for. This will only + * apply if there are no other limits that apply to the specific user-initiated job. + */ + public long RUNTIME_USER_INITIATED_LIMIT_MS = DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS; + + /** + * A factor to apply to estimated transfer durations for user-initiated data transfer jobs + * so that we give some extra time for unexpected situations. This will be at least 1 and + * so can just be multiplied with the original value to get the final value. + */ + public float RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = + DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR; + + /** + * The minimum amount of time we try to guarantee user-initiated data transfer jobs + * will run for. + */ + public long RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = + DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS; + + /** The maximum amount of time we will let a user-initiated data transfer job run for. */ + public long RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = + DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS; + /** * Whether to persist jobs in split files (by UID). If false, all persisted jobs will be * saved in a single file. @@ -801,7 +882,14 @@ public class JobSchedulerService extends com.android.server.SystemService DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, - KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS); + KEY_RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS, + KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR, + KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS, + KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + KEY_RUNTIME_USER_INITIATED_LIMIT_MS, + KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, + KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS); // Make sure min runtime for regular jobs is at least 10 minutes. RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS, @@ -819,6 +907,49 @@ public class JobSchedulerService extends com.android.server.SystemService RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS, properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)); + // Make sure min runtime is at least as long as regular jobs. + RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS, + properties.getLong( + KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + DEFAULT_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS)); + // Max limit should be at least the min guarantee AND the free quota. + RUNTIME_DATA_TRANSFER_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + Math.max(RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + properties.getLong( + KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS, + DEFAULT_RUNTIME_DATA_TRANSFER_LIMIT_MS))); + // Make sure min runtime is at least as long as regular jobs. + RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS, + properties.getLong( + KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + DEFAULT_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS)); + // Max limit should be at least the min guarantee AND the free quota. + RUNTIME_USER_INITIATED_LIMIT_MS = Math.max(RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + Math.max(RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + properties.getLong( + KEY_RUNTIME_USER_INITIATED_LIMIT_MS, + DEFAULT_RUNTIME_USER_INITIATED_LIMIT_MS))); + // The buffer factor should be at least 1 (so we don't decrease the time). + RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = Math.max(1, + properties.getFloat( + KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR, + DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR + )); + // Make sure min runtime is at least as long as other user-initiated jobs. + RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = Math.max( + RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + properties.getLong( + KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, + DEFAULT_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS)); + // Data transfer requires RUN_LONG_JOBS permission, so the upper limit will be higher + // than other jobs. + // Max limit should be the min guarantee and the max of other user-initiated jobs. + RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = Math.max( + RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, + Math.max(RUNTIME_USER_INITIATED_LIMIT_MS, + properties.getLong( + KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS, + DEFAULT_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS))); } private boolean updateTareSettingsLocked(boolean isTareEnabled) { @@ -867,6 +998,20 @@ public class JobSchedulerService extends com.android.server.SystemService RUNTIME_MIN_HIGH_PRIORITY_GUARANTEE_MS).println(); pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS) .println(); + pw.print(KEY_RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS).println(); + pw.print(KEY_RUNTIME_DATA_TRANSFER_LIMIT_MS, + RUNTIME_DATA_TRANSFER_LIMIT_MS).println(); + pw.print(KEY_RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS).println(); + pw.print(KEY_RUNTIME_USER_INITIATED_LIMIT_MS, + RUNTIME_USER_INITIATED_LIMIT_MS).println(); + pw.print(KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR, + RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR).println(); + pw.print(KEY_RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, + RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS).println(); + pw.print(KEY_RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS, + RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS).println(); pw.print(KEY_PERSIST_IN_SPLIT_FILES, PERSIST_IN_SPLIT_FILES).println(); @@ -1858,9 +2003,9 @@ public class JobSchedulerService extends com.android.server.SystemService final FlexibilityController flexibilityController = new FlexibilityController(this, mPrefetchController); mControllers.add(flexibilityController); - final ConnectivityController connectivityController = + mConnectivityController = new ConnectivityController(this, flexibilityController); - mControllers.add(connectivityController); + mControllers.add(mConnectivityController); mControllers.add(new TimeController(this)); final IdleController idleController = new IdleController(this, flexibilityController); mControllers.add(idleController); @@ -1876,16 +2021,16 @@ public class JobSchedulerService extends com.android.server.SystemService mDeviceIdleJobsController = new DeviceIdleJobsController(this); mControllers.add(mDeviceIdleJobsController); mQuotaController = - new QuotaController(this, backgroundJobsController, connectivityController); + new QuotaController(this, backgroundJobsController, mConnectivityController); mControllers.add(mQuotaController); mControllers.add(new ComponentController(this)); mTareController = - new TareController(this, backgroundJobsController, connectivityController); + new TareController(this, backgroundJobsController, mConnectivityController); mControllers.add(mTareController); mRestrictiveControllers = new ArrayList<>(); mRestrictiveControllers.add(batteryController); - mRestrictiveControllers.add(connectivityController); + mRestrictiveControllers.add(mConnectivityController); mRestrictiveControllers.add(idleController); // Create restrictions @@ -3028,7 +3173,30 @@ public class JobSchedulerService extends com.android.server.SystemService /** Returns the minimum amount of time we should let this job run before timing out. */ public long getMinJobExecutionGuaranteeMs(JobStatus job) { synchronized (mLock) { - if (job.shouldTreatAsExpeditedJob()) { + final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer() + && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName()); + if (job.shouldTreatAsUserInitiated()) { + if (shouldTreatAsDataTransfer) { + final long estimatedTransferTimeMs = + mConnectivityController.getEstimatedTransferTimeMs(job); + if (estimatedTransferTimeMs == ConnectivityController.UNKNOWN_TIME) { + return mConstants.RUNTIME_MIN_USER_INITIATED_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_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR); + return Math.min(mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS, + Math.max(factoredTransferTimeMs, + mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS + )); + } + return mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS; + } else if (shouldTreatAsDataTransfer) { + // For now, don't increase a bg data transfer's minimum guarantee. + return mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS; + } else if (job.shouldTreatAsExpeditedJob()) { // Don't guarantee RESTRICTED jobs more than 5 minutes. return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX ? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS @@ -3044,6 +3212,16 @@ public class JobSchedulerService extends com.android.server.SystemService /** Returns the maximum amount of time this job could run for. */ public long getMaxJobExecutionTimeMs(JobStatus job) { synchronized (mLock) { + final boolean shouldTreatAsDataTransfer = job.getJob().isDataTransfer() + && checkRunLongJobsPermission(job.getSourceUid(), job.getSourcePackageName()); + if (job.shouldTreatAsUserInitiated()) { + if (shouldTreatAsDataTransfer) { + return mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS; + } + return mConstants.RUNTIME_USER_INITIATED_LIMIT_MS; + } else if (shouldTreatAsDataTransfer) { + return mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS; + } return Math.min(mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mConstants.USE_TARE_POLICY ? mTareController.getMaxJobExecutionTimeMsLocked(job) @@ -3703,13 +3881,6 @@ public class JobSchedulerService extends com.android.server.SystemService return checkRunLongJobsPermission(uid, packageName); } - private boolean checkRunLongJobsPermission(int packageUid, String packageName) { - // Returns true if both the appop and permission are granted. - return PermissionChecker.checkPermissionForPreflight(getContext(), - android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN, - packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED; - } - /** * "dumpsys" infrastructure */ @@ -3985,13 +4156,27 @@ public class JobSchedulerService extends com.android.server.SystemService } } + private boolean checkRunLongJobsPermission(int packageUid, String packageName) { + // Returns true if both the appop and permission are granted. + return PermissionChecker.checkPermissionForPreflight(getTestableContext(), + android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN, + packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED; + } + + @VisibleForTesting + protected ConnectivityController getConnectivityController() { + return mConnectivityController; + } + // Shell command infrastructure int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) { try { final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); if (uid < 0) { - pw.print("unknown("); pw.print(pkgName); pw.println(")"); + pw.print("unknown("); + pw.print(pkgName); + pw.println(")"); return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index 16dd16727fa6e..6166921d64b22 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -42,7 +42,6 @@ import android.telephony.TelephonyManager; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.DataUnit; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Pools; @@ -82,6 +81,8 @@ public final class ConnectivityController extends RestrictingController implemen private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); + public static final long UNKNOWN_TIME = -1L; + // The networking stack has a hard limit so we can't make this configurable. private static final int MAX_NETWORK_CALLBACKS = 125; /** @@ -570,9 +571,8 @@ public final class ConnectivityController extends RestrictingController implemen // If we don't know the bandwidth, all we can do is hope the job finishes the minimum // chunk in time. if (bandwidthDown > 0) { - // Divide by 8 to convert bits to bytes. - final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS) - / (DataUnit.KIBIBYTES.toBytes(bandwidthDown) / 8)); + final long estimatedMillis = + calculateTransferTimeMs(minimumChunkBytes, bandwidthDown); if (estimatedMillis > maxJobExecutionTimeMs) { // If we'd never finish the minimum chunk before the timeout, we'd be insane! Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over " @@ -585,9 +585,8 @@ public final class ConnectivityController extends RestrictingController implemen final long bandwidthUp = capabilities.getLinkUpstreamBandwidthKbps(); // If we don't know the bandwidth, all we can do is hope the job finishes in time. if (bandwidthUp > 0) { - // Divide by 8 to convert bits to bytes. - final long estimatedMillis = ((minimumChunkBytes * DateUtils.SECOND_IN_MILLIS) - / (DataUnit.KIBIBYTES.toBytes(bandwidthUp) / 8)); + final long estimatedMillis = + calculateTransferTimeMs(minimumChunkBytes, bandwidthUp); if (estimatedMillis > maxJobExecutionTimeMs) { // If we'd never finish the minimum chunk before the timeout, we'd be insane! Slog.w(TAG, "Minimum chunk " + minimumChunkBytes + " bytes over " + bandwidthUp @@ -615,9 +614,7 @@ public final class ConnectivityController extends RestrictingController implemen final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps(); // If we don't know the bandwidth, all we can do is hope the job finishes in time. if (bandwidth > 0) { - // Divide by 8 to convert bits to bytes. - final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS) - / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8)); + final long estimatedMillis = calculateTransferTimeMs(downloadBytes, bandwidth); if (estimatedMillis > maxJobExecutionTimeMs) { // If we'd never finish before the timeout, we'd be insane! Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth @@ -633,9 +630,7 @@ public final class ConnectivityController extends RestrictingController implemen final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps(); // If we don't know the bandwidth, all we can do is hope the job finishes in time. if (bandwidth > 0) { - // Divide by 8 to convert bits to bytes. - final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS) - / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8)); + final long estimatedMillis = calculateTransferTimeMs(uploadBytes, bandwidth); if (estimatedMillis > maxJobExecutionTimeMs) { // If we'd never finish before the timeout, we'd be insane! Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth @@ -649,6 +644,48 @@ public final class ConnectivityController extends RestrictingController implemen return false; } + /** + * Return the estimated amount of time this job will be transferring data, + * based on the current network speed. + */ + public long getEstimatedTransferTimeMs(JobStatus jobStatus) { + final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); + final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes(); + if (downloadBytes == JobInfo.NETWORK_BYTES_UNKNOWN + && uploadBytes == JobInfo.NETWORK_BYTES_UNKNOWN) { + return UNKNOWN_TIME; + } + if (jobStatus.network == null) { + // This job doesn't have a network assigned. + return UNKNOWN_TIME; + } + NetworkCapabilities capabilities = getNetworkCapabilities(jobStatus.network); + if (capabilities == null) { + return UNKNOWN_TIME; + } + final long estimatedDownloadTimeMs = calculateTransferTimeMs(downloadBytes, + capabilities.getLinkDownstreamBandwidthKbps()); + final long estimatedUploadTimeMs = calculateTransferTimeMs(uploadBytes, + capabilities.getLinkUpstreamBandwidthKbps()); + if (estimatedDownloadTimeMs == UNKNOWN_TIME) { + return estimatedUploadTimeMs; + } else if (estimatedUploadTimeMs == UNKNOWN_TIME) { + return estimatedDownloadTimeMs; + } + return estimatedDownloadTimeMs + estimatedUploadTimeMs; + } + + @VisibleForTesting + static long calculateTransferTimeMs(long transferBytes, long bandwidthKbps) { + if (transferBytes == JobInfo.NETWORK_BYTES_UNKNOWN || bandwidthKbps <= 0) { + return UNKNOWN_TIME; + } + return (transferBytes * DateUtils.SECOND_IN_MILLIS) + // Multiply by 1000 to convert kilobits to bits. + // Divide by 8 to convert bits to bytes. + / (bandwidthKbps * 1000 / 8); + } + private static boolean isCongestionDelayed(JobStatus jobStatus, Network network, NetworkCapabilities capabilities, Constants constants) { // If network is congested, and job is less than 50% through the 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 83b6a8ea39166..419127e6c6a90 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 @@ -1344,6 +1344,15 @@ public final class JobStatus { return mExpeditedQuotaApproved && mExpeditedTareApproved && isRequestedExpeditedJob(); } + /** + * @return true if the job was scheduled as a user-initiated job and it hasn't been downgraded + * for any reason. + */ + public boolean shouldTreatAsUserInitiated() { + // TODO(248386641): implement + return false; + } + /** * Return a summary that uniquely identifies the underlying job. */ 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 fc737d06059d9..8e48490439847 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -34,6 +34,8 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.app.ActivityManager; @@ -46,6 +48,7 @@ import android.app.job.JobScheduler; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Context; +import android.content.PermissionChecker; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Resources; @@ -63,6 +66,7 @@ import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; import com.android.server.SystemServiceManager; +import com.android.server.job.controllers.ConnectivityController; import com.android.server.job.controllers.JobStatus; import com.android.server.pm.UserManagerInternal; import com.android.server.usage.AppStandbyInternal; @@ -102,6 +106,7 @@ public class JobSchedulerServiceTest { .initMocks(this) .strictness(Strictness.LENIENT) .mockStatic(LocalServices.class) + .mockStatic(PermissionChecker.class) .mockStatic(ServiceManager.class) .startMocking(); @@ -193,6 +198,15 @@ public class JobSchedulerServiceTest { jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag); } + private void grantRunLongJobsPermission(boolean grant) { + final int permissionStatus = grant + ? PermissionChecker.PERMISSION_GRANTED : PermissionChecker.PERMISSION_HARD_DENIED; + doReturn(permissionStatus) + .when(() -> PermissionChecker.checkPermissionForPreflight( + any(), eq(android.Manifest.permission.RUN_LONG_JOBS), + anyInt(), anyInt(), anyString())); + } + @Test public void testGetMinJobExecutionGuaranteeMs() { JobStatus ejMax = createJobStatus("testGetMinJobExecutionGuaranteeMs", @@ -207,6 +221,15 @@ public class JobSchedulerServiceTest { createJobInfo(5).setPriority(JobInfo.PRIORITY_HIGH)); JobStatus jobDef = createJobStatus("testGetMinJobExecutionGuaranteeMs", createJobInfo(6)); + JobStatus jobDT = createJobStatus("testGetMinJobExecutionGuaranteeMs", + createJobInfo(7) + .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobUI = createJobStatus("testGetMinJobExecutionGuaranteeMs", + createJobInfo(8)); // TODO(255371817): add setUserInitiated(true) + JobStatus jobUIDT = createJobStatus("testGetMinJobExecutionGuaranteeMs", + // TODO(255371817): add setUserInitiated(true) + createJobInfo(9) + .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); spyOn(ejMax); spyOn(ejHigh); @@ -214,6 +237,9 @@ public class JobSchedulerServiceTest { spyOn(ejHighDowngraded); spyOn(jobHigh); spyOn(jobDef); + spyOn(jobDT); + spyOn(jobUI); + spyOn(jobUIDT); when(ejMax.shouldTreatAsExpeditedJob()).thenReturn(true); when(ejHigh.shouldTreatAsExpeditedJob()).thenReturn(true); @@ -221,6 +247,16 @@ public class JobSchedulerServiceTest { when(ejHighDowngraded.shouldTreatAsExpeditedJob()).thenReturn(false); when(jobHigh.shouldTreatAsExpeditedJob()).thenReturn(false); when(jobDef.shouldTreatAsExpeditedJob()).thenReturn(false); + when(jobUI.shouldTreatAsUserInitiated()).thenReturn(true); + when(jobUIDT.shouldTreatAsUserInitiated()).thenReturn(true); + + ConnectivityController connectivityController = mService.getConnectivityController(); + spyOn(connectivityController); + mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS; + mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS = 60 * MINUTE_IN_MILLIS; + mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f; + mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS; + mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS; assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(ejMax)); @@ -234,8 +270,81 @@ public class JobSchedulerServiceTest { mService.getMinJobExecutionGuaranteeMs(jobHigh)); assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobDef)); + grantRunLongJobsPermission(false); // Without permission + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobDT)); + grantRunLongJobsPermission(true); // With permission + doReturn(ConnectivityController.UNKNOWN_TIME) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobDT)); + doReturn(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS / 2) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobDT)); + doReturn(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS * 2) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobDT)); + doReturn(mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS * 2) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobDT)); + // UserInitiated + assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUI)); + grantRunLongJobsPermission(false); + assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + grantRunLongJobsPermission(true); // With permission + doReturn(ConnectivityController.UNKNOWN_TIME) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + doReturn(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS / 2) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + doReturn(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS * 2) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals( + (long) (mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS + * 2 * 1.5), + mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + doReturn(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS * 2) + .when(connectivityController).getEstimatedTransferTimeMs(any()); + assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS, + mService.getMinJobExecutionGuaranteeMs(jobUIDT)); } + @Test + public void testGetMaxJobExecutionTimeMs() { + JobStatus jobDT = createJobStatus("testGetMaxJobExecutionTimeMs", + createJobInfo(7) + .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobUI = createJobStatus("testGetMaxJobExecutionTimeMs", + createJobInfo(9)); // TODO(255371817): add setUserInitiated(true) + JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs", + // TODO(255371817): add setUserInitiated(true) + createJobInfo(10) + .setDataTransfer(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + + spyOn(jobDT); + spyOn(jobUI); + spyOn(jobUIDT); + + when(jobUI.shouldTreatAsUserInitiated()).thenReturn(true); + when(jobUIDT.shouldTreatAsUserInitiated()).thenReturn(true); + + grantRunLongJobsPermission(true); + + assertEquals(mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobDT)); + assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUI)); + assertEquals(mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobUIDT)); + } /** * Confirm that {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int)} 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 1f85f2c2c46ee..42e22f3f7c2f0 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 @@ -24,6 +24,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -35,6 +36,7 @@ import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -240,20 +242,20 @@ public class ConnectivityControllerTest { .setLinkDownstreamBandwidthKbps(1).build(), mConstants)); // Slow downstream assertFalse(controller.isSatisfied(createJobStatus(job), net, - createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(137) + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(140) .setLinkDownstreamBandwidthKbps(1).build(), mConstants)); // Slow upstream assertFalse(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1) - .setLinkDownstreamBandwidthKbps(137).build(), mConstants)); + .setLinkDownstreamBandwidthKbps(140).build(), mConstants)); // Network good enough assertTrue(controller.isSatisfied(createJobStatus(job), net, - createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(137) - .setLinkDownstreamBandwidthKbps(137).build(), mConstants)); + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(140) + .setLinkDownstreamBandwidthKbps(140).build(), mConstants)); // Network slightly too slow given reduced time assertFalse(controller.isSatisfied(createJobStatus(job), net, - createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(130) - .setLinkDownstreamBandwidthKbps(130).build(), mConstants)); + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(139) + .setLinkDownstreamBandwidthKbps(139).build(), mConstants)); // Slow network is too slow, but device is charging and network is unmetered. when(mService.isBatteryCharging()).thenReturn(true); controller.onBatteryStateChangedLocked(); @@ -1188,6 +1190,78 @@ public class ConnectivityControllerTest { assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); } + @Test + public void testCalculateTransferTimeMs() { + assertEquals(ConnectivityController.UNKNOWN_TIME, + ConnectivityController.calculateTransferTimeMs(1, 0)); + assertEquals(ConnectivityController.UNKNOWN_TIME, + ConnectivityController.calculateTransferTimeMs(JobInfo.NETWORK_BYTES_UNKNOWN, 512)); + assertEquals(1, ConnectivityController.calculateTransferTimeMs(1, 8)); + assertEquals(1000, ConnectivityController.calculateTransferTimeMs(1000, 8)); + assertEquals(8, ConnectivityController.calculateTransferTimeMs(1024, 1024)); + } + + @Test + public void testGetEstimatedTransferTimeMs() { + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture()); + + final JobStatus job = createJobStatus(createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(10_000), + DataUnit.MEBIBYTES.toBytes(1_000)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + + final ConnectivityController controller = new ConnectivityController(mService, + mFlexibilityController); + + final JobStatus jobNoEstimates = createJobStatus(createJob()); + assertEquals(ConnectivityController.UNKNOWN_TIME, + controller.getEstimatedTransferTimeMs(jobNoEstimates)); + + // No network + job.network = null; + assertEquals(ConnectivityController.UNKNOWN_TIME, + controller.getEstimatedTransferTimeMs(job)); + + final NetworkCallback generalCallback = callbackCaptor.getValue(); + + // No capabilities + final Network network = mock(Network.class); + answerNetwork(generalCallback, null, null, network, null); + job.network = network; + assertEquals(ConnectivityController.UNKNOWN_TIME, + controller.getEstimatedTransferTimeMs(job)); + + // Capabilities don't have bandwidth values + NetworkCapabilities caps = createCapabilitiesBuilder().build(); + answerNetwork(generalCallback, null, null, network, caps); + assertEquals(ConnectivityController.UNKNOWN_TIME, + controller.getEstimatedTransferTimeMs(job)); + + // Capabilities only has downstream bandwidth + caps = createCapabilitiesBuilder() + .setLinkDownstreamBandwidthKbps(1024) + .build(); + answerNetwork(generalCallback, null, null, network, caps); + assertEquals(81920 * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job)); + + // Capabilities only has upstream bandwidth + caps = createCapabilitiesBuilder() + .setLinkUpstreamBandwidthKbps(2 * 1024) + .build(); + answerNetwork(generalCallback, null, null, network, caps); + assertEquals(4096 * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job)); + + // Capabilities only both stream bandwidths + caps = createCapabilitiesBuilder() + .setLinkDownstreamBandwidthKbps(1024) + .setLinkUpstreamBandwidthKbps(2 * 1024) + .build(); + answerNetwork(generalCallback, null, null, network, caps); + assertEquals((81920 + 4096) * SECOND_IN_MILLIS, controller.getEstimatedTransferTimeMs(job)); + } + private void answerNetwork(@NonNull NetworkCallback generalCallback, @Nullable NetworkCallback uidCallback, @Nullable Network lastNetwork, @Nullable Network net, @Nullable NetworkCapabilities caps) { @@ -1198,11 +1272,15 @@ public class ConnectivityControllerTest { } } else { generalCallback.onAvailable(net); - generalCallback.onCapabilitiesChanged(net, caps); + if (caps != null) { + generalCallback.onCapabilitiesChanged(net, caps); + } if (uidCallback != null) { uidCallback.onAvailable(net); uidCallback.onBlockedStatusChanged(net, ConnectivityManager.BLOCKED_REASON_NONE); - uidCallback.onCapabilitiesChanged(net, caps); + if (caps != null) { + uidCallback.onCapabilitiesChanged(net, caps); + } } } }