Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c14d4337 authored by Kweku Adams's avatar Kweku Adams
Browse files

Allow apps to specify minimum chunk size.

JobScheduler wouldn't start a connectivity job if the app gave an estimated
download/upload size and JS calculated that the job wouldn't complete
successfully within the max execution time. The logic assumed that apps
don't support interruptible work and that downloads/uploads are
all-or-nothing. This negatively impacted apps that support
interruptible/resume downloads & uploads.

Adding an API to let an app indicate it supports resumable network
activity so that JS can be smarter about whether to start a job or not.

Bug: 188429037
Test: atest CtsJobSchedulerTestCases:JobInfoTest
Test: atest FrameworksMockingServicesTests:ConnectivityControllerTest
Change-Id: I76bfe8d1ad3ffedd7ce66e7e31098db089749c83
parent 7a1c5d38
Loading
Loading
Loading
Loading
+74 −3
Original line number Diff line number Diff line
@@ -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<TriggerContentUri> 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)}.
         *
         * <p>
         * 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}.
         *
         * <p>
         * 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) {
+79 −5
Original line number Diff line number Diff line
@@ -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}.
     * <p>
     * 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();
    }
+1 −0
Original line number Diff line number Diff line
@@ -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();
+41 −3
Original line number Diff line number Diff line
@@ -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) {
+22 −6
Original line number Diff line number Diff line
@@ -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);
Loading