diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 144536ec2644d..646a0279c7224 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -324,6 +324,7 @@ public class JobInfo implements Parcelable { private final NetworkRequest networkRequest; private final long networkDownloadBytes; private final long networkUploadBytes; + private final long minimumNetworkChunkBytes; private final long minLatencyMillis; private final long maxExecutionDelayMillis; private final boolean isPeriodic; @@ -514,6 +515,17 @@ public class JobInfo implements Parcelable { return networkUploadBytes; } + /** + * Return the smallest piece of data that cannot be easily paused and resumed, in bytes. + * + * @return Smallest piece of data that cannot be easily paused and resumed, or + * {@link #NETWORK_BYTES_UNKNOWN} when unknown. + * @see Builder#setMinimumNetworkChunkBytes(long) + */ + public @BytesLong long getMinimumNetworkChunkBytes() { + return minimumNetworkChunkBytes; + } + /** * Set for a job that does not recur periodically, to specify a delay after which the job * will be eligible for execution. This value is not set if the job recurs periodically. @@ -679,6 +691,9 @@ public class JobInfo implements Parcelable { if (networkUploadBytes != j.networkUploadBytes) { return false; } + if (minimumNetworkChunkBytes != j.minimumNetworkChunkBytes) { + return false; + } if (minLatencyMillis != j.minLatencyMillis) { return false; } @@ -741,6 +756,7 @@ public class JobInfo implements Parcelable { } hashCode = 31 * hashCode + Long.hashCode(networkDownloadBytes); hashCode = 31 * hashCode + Long.hashCode(networkUploadBytes); + hashCode = 31 * hashCode + Long.hashCode(minimumNetworkChunkBytes); hashCode = 31 * hashCode + Long.hashCode(minLatencyMillis); hashCode = 31 * hashCode + Long.hashCode(maxExecutionDelayMillis); hashCode = 31 * hashCode + Boolean.hashCode(isPeriodic); @@ -777,6 +793,7 @@ public class JobInfo implements Parcelable { } networkDownloadBytes = in.readLong(); networkUploadBytes = in.readLong(); + minimumNetworkChunkBytes = in.readLong(); minLatencyMillis = in.readLong(); maxExecutionDelayMillis = in.readLong(); isPeriodic = in.readInt() == 1; @@ -807,6 +824,7 @@ public class JobInfo implements Parcelable { networkRequest = b.mNetworkRequest; networkDownloadBytes = b.mNetworkDownloadBytes; networkUploadBytes = b.mNetworkUploadBytes; + minimumNetworkChunkBytes = b.mMinimumNetworkChunkBytes; minLatencyMillis = b.mMinLatencyMillis; maxExecutionDelayMillis = b.mMaxExecutionDelayMillis; isPeriodic = b.mIsPeriodic; @@ -851,6 +869,7 @@ public class JobInfo implements Parcelable { } out.writeLong(networkDownloadBytes); out.writeLong(networkUploadBytes); + out.writeLong(minimumNetworkChunkBytes); out.writeLong(minLatencyMillis); out.writeLong(maxExecutionDelayMillis); out.writeInt(isPeriodic ? 1 : 0); @@ -986,6 +1005,7 @@ public class JobInfo implements Parcelable { private NetworkRequest mNetworkRequest; private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + private long mMinimumNetworkChunkBytes = NETWORK_BYTES_UNKNOWN; private ArrayList mTriggerContentUris; private long mTriggerContentUpdateDelay = -1; private long mTriggerContentMaxDelay = -1; @@ -1038,6 +1058,7 @@ public class JobInfo implements Parcelable { mNetworkRequest = job.getRequiredNetwork(); mNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes(); mNetworkUploadBytes = job.getEstimatedNetworkUploadBytes(); + mMinimumNetworkChunkBytes = job.getMinimumNetworkChunkBytes(); mTriggerContentUris = job.getTriggerContentUris() != null ? new ArrayList<>(Arrays.asList(job.getTriggerContentUris())) : null; mTriggerContentUpdateDelay = job.getTriggerContentUpdateDelay(); @@ -1255,6 +1276,39 @@ public class JobInfo implements Parcelable { return this; } + /** + * Set the minimum size of non-resumable network traffic this job requires, in bytes. When + * the upload or download can be easily paused and resumed, use this to set the smallest + * size that must be transmitted between start and stop events to be considered successful. + * If the transfer cannot be paused and resumed, then this should be the sum of the values + * provided to {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)}. + * + *

+ * Apps are encouraged to provide values that are as accurate as possible since JobScheduler + * will try to run the job at a time when at least the minimum chunk can be transmitted to + * reduce the amount of repetitive data that's transferred. Jobs that cannot provide + * reasonable estimates should use the sentinel value {@link JobInfo#NETWORK_BYTES_UNKNOWN}. + * + *

+ * The values provided here only reflect the minimum non-resumable traffic that will be + * performed by the base job; if you're using {@link JobWorkItem} then + * you also need to define the network traffic used by each work item + * when constructing them. + * + * @param chunkSizeBytes The smallest piece of data that cannot be easily paused and + * resumed, in bytes. + * @see JobInfo#getMinimumNetworkChunkBytes() + * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long, long) + */ + @NonNull + public Builder setMinimumNetworkChunkBytes(@BytesLong long chunkSizeBytes) { + if (chunkSizeBytes != NETWORK_BYTES_UNKNOWN && chunkSizeBytes <= 0) { + throw new IllegalArgumentException("Minimum chunk size must be positive"); + } + mMinimumNetworkChunkBytes = chunkSizeBytes; + return this; + } + /** * Specify that to run this job, the device must be charging (or be a * non-battery-powered device connected to permanent power, such as Android TV @@ -1647,12 +1701,29 @@ public class JobInfo implements Parcelable { /** * @hide */ - public void enforceValidity() { - // Check that network estimates require network type - if ((networkDownloadBytes > 0 || networkUploadBytes > 0) && networkRequest == null) { + public final void enforceValidity() { + // Check that network estimates require network type and are reasonable values. + if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0) + && networkRequest == null) { throw new IllegalArgumentException( "Can't provide estimated network usage without requiring a network"); } + final long estimatedTransfer; + if (networkUploadBytes == NETWORK_BYTES_UNKNOWN) { + estimatedTransfer = networkDownloadBytes; + } else { + estimatedTransfer = networkUploadBytes + + (networkDownloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : networkDownloadBytes); + } + if (minimumNetworkChunkBytes != NETWORK_BYTES_UNKNOWN + && estimatedTransfer != NETWORK_BYTES_UNKNOWN + && minimumNetworkChunkBytes > estimatedTransfer) { + throw new IllegalArgumentException( + "Minimum chunk size can't be greater than estimated network usage"); + } + if (minimumNetworkChunkBytes != NETWORK_BYTES_UNKNOWN && minimumNetworkChunkBytes <= 0) { + throw new IllegalArgumentException("Minimum chunk size must be positive"); + } // Check that a deadline was not set on a periodic job. if (isPeriodic) { diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java index 0c45cbf6dc114..372f9faacd5a2 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java @@ -19,6 +19,7 @@ package android.app.job; import static android.app.job.JobInfo.NETWORK_BYTES_UNKNOWN; import android.annotation.BytesLong; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Intent; import android.os.Build; @@ -33,8 +34,9 @@ import android.os.Parcelable; final public class JobWorkItem implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) final Intent mIntent; - final long mNetworkDownloadBytes; - final long mNetworkUploadBytes; + private final long mNetworkDownloadBytes; + private final long mNetworkUploadBytes; + private final long mMinimumChunkBytes; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) int mDeliveryCount; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) @@ -49,9 +51,7 @@ final public class JobWorkItem implements Parcelable { * @param intent The general Intent describing this work. */ public JobWorkItem(Intent intent) { - mIntent = intent; - mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; - mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + this(intent, NETWORK_BYTES_UNKNOWN, NETWORK_BYTES_UNKNOWN); } /** @@ -68,9 +68,45 @@ final public class JobWorkItem implements Parcelable { * uploaded by this job work item, in bytes. */ public JobWorkItem(Intent intent, @BytesLong long downloadBytes, @BytesLong long uploadBytes) { + this(intent, downloadBytes, uploadBytes, NETWORK_BYTES_UNKNOWN); + } + + /** + * Create a new piece of work, which can be submitted to + * {@link JobScheduler#enqueue JobScheduler.enqueue}. + *

+ * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for + * details about how to estimate network traffic. + * + * @param intent The general Intent describing this work. + * @param downloadBytes The estimated size of network traffic that will be + * downloaded by this job work item, in bytes. + * @param uploadBytes The estimated size of network traffic that will be + * uploaded by this job work item, in bytes. + * @param minimumChunkBytes The smallest piece of data that cannot be easily paused and + * resumed, in bytes. + */ + public JobWorkItem(@Nullable Intent intent, @BytesLong long downloadBytes, + @BytesLong long uploadBytes, @BytesLong long minimumChunkBytes) { + if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && minimumChunkBytes <= 0) { + throw new IllegalArgumentException("Minimum chunk size must be positive"); + } + final long estimatedTransfer; + if (uploadBytes == NETWORK_BYTES_UNKNOWN) { + estimatedTransfer = downloadBytes; + } else { + estimatedTransfer = uploadBytes + + (downloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : downloadBytes); + } + if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && estimatedTransfer != NETWORK_BYTES_UNKNOWN + && minimumChunkBytes > estimatedTransfer) { + throw new IllegalArgumentException( + "Minimum chunk size can't be greater than estimated network usage"); + } mIntent = intent; mNetworkDownloadBytes = downloadBytes; mNetworkUploadBytes = uploadBytes; + mMinimumChunkBytes = minimumChunkBytes; } /** @@ -102,6 +138,16 @@ final public class JobWorkItem implements Parcelable { return mNetworkUploadBytes; } + /** + * Return the smallest piece of data that cannot be easily paused and resumed, in bytes. + * + * @return Smallest piece of data that cannot be easily paused and resumed, or + * {@link JobInfo#NETWORK_BYTES_UNKNOWN} when unknown. + */ + public @BytesLong long getMinimumNetworkChunkBytes() { + return mMinimumChunkBytes; + } + /** * Return the count of the number of times this work item has been delivered * to the job. The value will be > 1 if it has been redelivered because the job @@ -161,6 +207,10 @@ final public class JobWorkItem implements Parcelable { sb.append(" uploadBytes="); sb.append(mNetworkUploadBytes); } + if (mMinimumChunkBytes != NETWORK_BYTES_UNKNOWN) { + sb.append(" minimumChunkBytes="); + sb.append(mMinimumChunkBytes); + } if (mDeliveryCount != 0) { sb.append(" dcount="); sb.append(mDeliveryCount); @@ -169,6 +219,28 @@ final public class JobWorkItem implements Parcelable { return sb.toString(); } + /** + * @hide + */ + public void enforceValidity() { + final long estimatedTransfer; + if (mNetworkUploadBytes == NETWORK_BYTES_UNKNOWN) { + estimatedTransfer = mNetworkDownloadBytes; + } else { + estimatedTransfer = mNetworkUploadBytes + + (mNetworkDownloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : mNetworkDownloadBytes); + } + if (mMinimumChunkBytes != NETWORK_BYTES_UNKNOWN + && estimatedTransfer != NETWORK_BYTES_UNKNOWN + && mMinimumChunkBytes > estimatedTransfer) { + throw new IllegalArgumentException( + "Minimum chunk size can't be greater than estimated network usage"); + } + if (mMinimumChunkBytes != NETWORK_BYTES_UNKNOWN && mMinimumChunkBytes <= 0) { + throw new IllegalArgumentException("Minimum chunk size must be positive"); + } + } + public int describeContents() { return 0; } @@ -182,6 +254,7 @@ final public class JobWorkItem implements Parcelable { } out.writeLong(mNetworkDownloadBytes); out.writeLong(mNetworkUploadBytes); + out.writeLong(mMinimumChunkBytes); out.writeInt(mDeliveryCount); out.writeInt(mWorkId); } @@ -206,6 +279,7 @@ final public class JobWorkItem implements Parcelable { } mNetworkDownloadBytes = in.readLong(); mNetworkUploadBytes = in.readLong(); + mMinimumChunkBytes = in.readLong(); mDeliveryCount = in.readInt(); mWorkId = in.readInt(); } 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 72d8e721d0477..f3b2a962bbe83 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -2806,6 +2806,7 @@ public class JobSchedulerService extends com.android.server.SystemService throw new NullPointerException("work is null"); } + work.enforceValidity(); validateJobFlags(job, uid); final long ident = Binder.clearCallingIdentity(); 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 41ff9c8e3b7eb..9a257725b02b5 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 @@ -549,6 +549,47 @@ public final class ConnectivityController extends RestrictingController implemen */ private boolean isInsane(JobStatus jobStatus, Network network, NetworkCapabilities capabilities, Constants constants) { + // Use the maximum possible time since it gives us an upper bound, even though the job + // could end up stopping earlier. + final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); + + final long minimumChunkBytes = jobStatus.getMinimumNetworkChunkBytes(); + if (minimumChunkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + final long bandwidthDown = capabilities.getLinkDownstreamBandwidthKbps(); + // 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)); + 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 " + + bandwidthDown + " kbps network would take " + + estimatedMillis + "ms and job has " + + maxJobExecutionTimeMs + "ms to run; that's insane!"); + return true; + } + } + 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)); + 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 + + " kbps network would take " + estimatedMillis + "ms and job has " + + maxJobExecutionTimeMs + "ms to run; that's insane!"); + return true; + } + } + return false; + } + + // Minimum chunk size isn't defined. Check using the estimated upload/download sizes. + if (capabilities.hasCapability(NET_CAPABILITY_NOT_METERED) && mChargingTracker.isCharging()) { // We're charging and on an unmetered network. We don't have to be as conservative about @@ -557,9 +598,6 @@ public final class ConnectivityController extends RestrictingController implemen return false; } - // Use the maximum possible time since it gives us an upper bound, even though the job - // could end up stopping earlier. - final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus); final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes(); if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { 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 95eb220cfeb40..fda57f72eb08c 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 @@ -391,6 +391,7 @@ public final class JobStatus { private long mTotalNetworkDownloadBytes = JobInfo.NETWORK_BYTES_UNKNOWN; private long mTotalNetworkUploadBytes = JobInfo.NETWORK_BYTES_UNKNOWN; + private long mMinimumNetworkChunkBytes = JobInfo.NETWORK_BYTES_UNKNOWN; /////// Booleans that track if a job is ready to run. They should be updated whenever dependent /////// states change. @@ -531,7 +532,7 @@ public final class JobStatus { mInternalFlags = internalFlags; - updateEstimatedNetworkBytesLocked(); + updateNetworkBytesLocked(); if (job.getRequiredNetwork() != null) { // Later, when we check if a given network satisfies the required @@ -664,7 +665,7 @@ public final class JobStatus { sourcePackageName, sourceUserId, toShortString())); } pendingWork.add(work); - updateEstimatedNetworkBytesLocked(); + updateNetworkBytesLocked(); } public JobWorkItem dequeueWorkLocked() { @@ -677,7 +678,7 @@ public final class JobStatus { executingWork.add(work); work.bumpDeliveryCount(); } - updateEstimatedNetworkBytesLocked(); + updateNetworkBytesLocked(); return work; } return null; @@ -736,7 +737,7 @@ public final class JobStatus { pendingWork = null; executingWork = null; incomingJob.nextPendingWorkId = nextPendingWorkId; - incomingJob.updateEstimatedNetworkBytesLocked(); + incomingJob.updateNetworkBytesLocked(); } else { // We are completely stopping the job... need to clean up work. ungrantWorkList(pendingWork); @@ -744,7 +745,7 @@ public final class JobStatus { ungrantWorkList(executingWork); executingWork = null; } - updateEstimatedNetworkBytesLocked(); + updateNetworkBytesLocked(); } public void prepareLocked() { @@ -944,9 +945,10 @@ public final class JobStatus { } } - private void updateEstimatedNetworkBytesLocked() { + private void updateNetworkBytesLocked() { mTotalNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes(); mTotalNetworkUploadBytes = job.getEstimatedNetworkUploadBytes(); + mMinimumNetworkChunkBytes = job.getMinimumNetworkChunkBytes(); if (pendingWork != null) { for (int i = 0; i < pendingWork.size(); i++) { @@ -968,6 +970,12 @@ public final class JobStatus { mTotalNetworkUploadBytes += uploadBytes; } } + final long chunkBytes = pendingWork.get(i).getMinimumNetworkChunkBytes(); + if (mMinimumNetworkChunkBytes == JobInfo.NETWORK_BYTES_UNKNOWN) { + mMinimumNetworkChunkBytes = chunkBytes; + } else if (chunkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + mMinimumNetworkChunkBytes = Math.min(mMinimumNetworkChunkBytes, chunkBytes); + } } } } @@ -980,6 +988,10 @@ public final class JobStatus { return mTotalNetworkUploadBytes; } + public long getMinimumNetworkChunkBytes() { + return mMinimumNetworkChunkBytes; + } + /** Does this job have any sort of networking constraint? */ public boolean hasConnectivityConstraint() { // No need to check mDynamicConstraints since connectivity will only be in that list if @@ -1942,6 +1954,10 @@ public final class JobStatus { pw.print("Network upload bytes: "); pw.println(mTotalNetworkUploadBytes); } + if (mMinimumNetworkChunkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) { + pw.print("Minimum network chunk bytes: "); + pw.println(mMinimumNetworkChunkBytes); + } if (job.getMinLatencyMillis() != 0) { pw.print("Minimum latency: "); TimeUtils.formatDuration(job.getMinLatencyMillis(), pw); diff --git a/core/api/current.txt b/core/api/current.txt index 3d88122cd11bc..254338af86883 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7937,6 +7937,7 @@ package android.app.job { method public static final long getMinFlexMillis(); method public long getMinLatencyMillis(); method public static final long getMinPeriodMillis(); + method public long getMinimumNetworkChunkBytes(); method @Deprecated public int getNetworkType(); method @Nullable public android.net.NetworkRequest getRequiredNetwork(); method @NonNull public android.content.ComponentName getService(); @@ -7979,6 +7980,7 @@ package android.app.job { method public android.app.job.JobInfo.Builder setExtras(@NonNull android.os.PersistableBundle); method @Deprecated public android.app.job.JobInfo.Builder setImportantWhileForeground(boolean); method public android.app.job.JobInfo.Builder setMinimumLatency(long); + method @NonNull public android.app.job.JobInfo.Builder setMinimumNetworkChunkBytes(long); method public android.app.job.JobInfo.Builder setOverrideDeadline(long); method public android.app.job.JobInfo.Builder setPeriodic(long); method public android.app.job.JobInfo.Builder setPeriodic(long, long); @@ -8071,11 +8073,13 @@ package android.app.job { public final class JobWorkItem implements android.os.Parcelable { ctor public JobWorkItem(android.content.Intent); ctor public JobWorkItem(android.content.Intent, long, long); + ctor public JobWorkItem(@Nullable android.content.Intent, long, long, long); method public int describeContents(); method public int getDeliveryCount(); method public long getEstimatedNetworkDownloadBytes(); method public long getEstimatedNetworkUploadBytes(); method public android.content.Intent getIntent(); + method public long getMinimumNetworkChunkBytes(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; } 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 c40742931a94a..a9853bf94eebd 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 @@ -152,6 +152,11 @@ public class ConnectivityControllerTest { .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), DataUnit.MEBIBYTES.toBytes(1)) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + final JobInfo.Builder jobWithMinChunk = createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), + DataUnit.MEBIBYTES.toBytes(1)) + .setMinimumNetworkChunkBytes(DataUnit.KIBIBYTES.toBytes(100)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); final ArgumentCaptor chargingCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); @@ -170,18 +175,51 @@ public class ConnectivityControllerTest { assertFalse(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1) .setLinkDownstreamBandwidthKbps(1).build(), mConstants)); + assertFalse(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1) + .setLinkDownstreamBandwidthKbps(1).build(), mConstants)); // Slow downstream assertFalse(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1024) .setLinkDownstreamBandwidthKbps(1).build(), mConstants)); + assertFalse(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1024) + .setLinkDownstreamBandwidthKbps(1).build(), mConstants)); // Slow upstream assertFalse(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1) .setLinkDownstreamBandwidthKbps(1024).build(), mConstants)); + assertFalse(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1) + .setLinkDownstreamBandwidthKbps(1024).build(), mConstants)); + // Medium network is fine for min chunk + assertFalse(controller.isSatisfied(createJobStatus(job), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(5) + .setLinkDownstreamBandwidthKbps(5).build(), mConstants)); + assertTrue(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(5) + .setLinkDownstreamBandwidthKbps(5).build(), mConstants)); + // Medium downstream + assertFalse(controller.isSatisfied(createJobStatus(job), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1024) + .setLinkDownstreamBandwidthKbps(5).build(), mConstants)); + assertTrue(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1024) + .setLinkDownstreamBandwidthKbps(5).build(), mConstants)); + // Medium upstream + assertFalse(controller.isSatisfied(createJobStatus(job), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(5) + .setLinkDownstreamBandwidthKbps(1024).build(), mConstants)); + assertTrue(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(5) + .setLinkDownstreamBandwidthKbps(1024).build(), mConstants)); // Fast network looks great assertTrue(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1024) .setLinkDownstreamBandwidthKbps(1024).build(), mConstants)); + assertTrue(controller.isSatisfied(createJobStatus(jobWithMinChunk), net, + createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(1024) + .setLinkDownstreamBandwidthKbps(1024).build(), mConstants)); // Slow network still good given time assertTrue(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().setLinkUpstreamBandwidthKbps(130) @@ -241,7 +279,6 @@ public class ConnectivityControllerTest { final ConnectivityController controller = new ConnectivityController(mService); when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L); - // Suspended networks aren't usable. assertFalse(controller.isSatisfied(createJobStatus(job), net, createCapabilitiesBuilder().removeCapability(NET_CAPABILITY_NOT_SUSPENDED)