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

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

First version of HPJs.

This is the base implementation for HPJs. It allows scheduling and
running of HPJs with their own quota. Quota is regained when previous
sessions expire and when certain events take place.

A few things still aren't implemented yet. They'll be done in a separate
CL:
1. Free temp-allowlist jobs
2. Dedicated JobServiceContexts for HPJs
3. Additional quota for system-critical roles

Bug: 171305774
Test: atest --rerun-until-failure 5 com.android.server.job.controllers.QuotaControllerTest
Test: atest CtsJobSchedulerTestCases
Change-Id: I4b704cb0b406769f6de8b60bf762dd1f4eb10c68
parent 649ed103
Loading
Loading
Loading
Loading
+76 −1
Original line number Diff line number Diff line
@@ -274,12 +274,19 @@ public class JobInfo implements Parcelable {

    /**
     * This job needs to be exempted from the app standby throttling. Only the system (UID 1000)
     * can set it. Jobs with a time constrant must not have it.
     * can set it. Jobs with a time constraint must not have it.
     *
     * @hide
     */
    public static final int FLAG_EXEMPT_FROM_APP_STANDBY = 1 << 3;

    /**
     * Whether it's a so-called "HPJ" or not.
     *
     * @hide
     */
    public static final int FLAG_FOREGROUND_JOB = 1 << 4;

    /**
     * @hide
     */
@@ -571,12 +578,20 @@ public class JobInfo implements Parcelable {

    /**
     * Return the backoff policy of this job.
     *
     * @see JobInfo.Builder#setBackoffCriteria(long, int)
     */
    public @BackoffPolicy int getBackoffPolicy() {
        return backoffPolicy;
    }

    /**
     * @see JobInfo.Builder#setForeground(boolean)
     */
    public boolean isForegroundJob() {
        return (flags & FLAG_FOREGROUND_JOB) != 0;
    }

    /**
     * @see JobInfo.Builder#setImportantWhileForeground(boolean)
     */
@@ -1441,6 +1456,41 @@ public class JobInfo implements Parcelable {
            return this;
        }

        /**
         * Setting this to true indicates that this job is important and needs to run as soon as
         * possible with stronger guarantees than regular jobs. These "foreground" jobs will:
         * <ol>
         *     <li>Run as soon as possible</li>
         *     <li>Be exempted from Doze and battery saver restrictions</li>
         *     <li>Have network access</li>
         * </ol>
         *
         * Since these jobs have stronger guarantees than regular jobs, they will be subject to
         * stricter quotas. As long as an app has available foreground quota, jobs scheduled with
         * this set to true will run with these guarantees. If an app has run out of available
         * foreground quota, any pending foreground jobs will run as regular jobs.
         * {@link JobParameters#isForegroundJob()} can be used to know whether the executing job
         * has foreground guarantees or not. In addition, {@link JobScheduler#schedule(JobInfo)}
         * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have
         * available quota (and the job will not be successfully scheduled).
         *
         * Foreground jobs may only set network constraints. No other constraints are allowed.
         *
         * Note: Even though foreground jobs are meant to run as soon as possible, they may be
         * deferred if the system is under heavy load or the network constraint is satisfied
         *
         * @see JobInfo#isForegroundJob()
         */
        @NonNull
        public Builder setForeground(boolean foreground) {
            if (foreground) {
                mFlags |= FLAG_FOREGROUND_JOB;
            } else {
                mFlags &= (~FLAG_FOREGROUND_JOB);
            }
            return this;
        }

        /**
         * Setting this to true indicates that this job is important while the scheduling app
         * is in the foreground or on the temporary whitelist for background restrictions.
@@ -1456,7 +1506,9 @@ public class JobInfo implements Parcelable {
         * @param importantWhileForeground whether to relax doze restrictions for this job when the
         *                                 app is in the foreground. False by default.
         * @see JobInfo#isImportantWhileForeground()
         * @deprecated Use {@link #setForeground(boolean)} instead.
         */
        @Deprecated
        public Builder setImportantWhileForeground(boolean importantWhileForeground) {
            if (importantWhileForeground) {
                mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
@@ -1580,6 +1632,29 @@ public class JobInfo implements Parcelable {
            throw new IllegalArgumentException(
                    "An important while foreground job cannot have a time delay");
        }

        if ((flags & FLAG_FOREGROUND_JOB) != 0) {
            if (hasEarlyConstraint) {
                throw new IllegalArgumentException("A foreground job cannot have a time delay");
            }
            if (hasLateConstraint) {
                throw new IllegalArgumentException("A foreground job cannot have a deadline");
            }
            if (isPeriodic) {
                throw new IllegalArgumentException("A foreground job cannot be periodic");
            }
            if (isPersisted) {
                throw new IllegalArgumentException("A foreground job cannot be persisted");
            }
            if (constraintFlags != 0 || (flags & ~FLAG_FOREGROUND_JOB) != 0) {
                throw new IllegalArgumentException(
                        "A foreground job can only have network constraints");
            }
            if (triggerContentUris != null && triggerContentUris.length > 0) {
                throw new IllegalArgumentException(
                        "Can't call addTriggerContentUri() on a foreground job");
            }
        }
    }

    /**
+18 −1
Original line number Diff line number Diff line
@@ -111,6 +111,9 @@ public class JobParameters implements Parcelable {
    @UnsupportedAppUsage
    private final IBinder callback;
    private final boolean overrideDeadlineExpired;
    // HPJs = foreground jobs.
    // TODO(171305774): clean up naming
    private final boolean mIsHpj;
    private final Uri[] mTriggeredContentUris;
    private final String[] mTriggeredContentAuthorities;
    private final Network network;
@@ -121,7 +124,7 @@ public class JobParameters implements Parcelable {
    /** @hide */
    public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
            Bundle transientExtras, ClipData clipData, int clipGrantFlags,
            boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
            boolean overrideDeadlineExpired, boolean isHpj, Uri[] triggeredContentUris,
            String[] triggeredContentAuthorities, Network network) {
        this.jobId = jobId;
        this.extras = extras;
@@ -130,6 +133,7 @@ public class JobParameters implements Parcelable {
        this.clipGrantFlags = clipGrantFlags;
        this.callback = callback;
        this.overrideDeadlineExpired = overrideDeadlineExpired;
        this.mIsHpj = isHpj;
        this.mTriggeredContentUris = triggeredContentUris;
        this.mTriggeredContentAuthorities = triggeredContentAuthorities;
        this.network = network;
@@ -194,6 +198,17 @@ public class JobParameters implements Parcelable {
        return clipGrantFlags;
    }

    /**
     * @return Whether this job is running as a foreground job or not. A job is guaranteed to have
     * all foreground job guarantees for the duration of the job execution if this returns
     * {@code true}. This will return {@code false} if the job that wasn't requested to run as a
     * foreground job, or if it was requested to run as a foreground job but the app didn't have
     * any remaining foreground job quota at the time of execution.
     */
    public boolean isForegroundJob() {
        return mIsHpj;
    }

    /**
     * For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this
     * provides an easy way to tell whether the job is being executed due to the deadline
@@ -337,6 +352,7 @@ public class JobParameters implements Parcelable {
        }
        callback = in.readStrongBinder();
        overrideDeadlineExpired = in.readInt() == 1;
        mIsHpj = in.readBoolean();
        mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
        mTriggeredContentAuthorities = in.createStringArray();
        if (in.readInt() != 0) {
@@ -373,6 +389,7 @@ public class JobParameters implements Parcelable {
        }
        dest.writeStrongBinder(callback);
        dest.writeInt(overrideDeadlineExpired ? 1 : 0);
        dest.writeBoolean(mIsHpj);
        dest.writeTypedArray(mTriggeredContentUris, flags);
        dest.writeStringArray(mTriggeredContentAuthorities);
        if (network != null) {
+5 −1
Original line number Diff line number Diff line
@@ -205,7 +205,9 @@ class JobConcurrencyManager {
    }

    private boolean isFgJob(JobStatus job) {
        return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP;
        // (It's super confusing PRIORITY_BOUND_FOREGROUND_SERVICE isn't FG here)
        return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP
                || job.shouldTreatAsForegroundJob();
    }

    @GuardedBy("mLock")
@@ -336,6 +338,8 @@ class JobConcurrencyManager {
                continue;
            }

            // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them

            final boolean isPendingFg = isFgJob(nextPending);

            // Find an available slot for nextPending. The context should be available OR
+22 −5
Original line number Diff line number Diff line
@@ -834,6 +834,13 @@ public class JobSchedulerService extends com.android.server.SystemService
            // Higher override state (OVERRIDE_FULL) should be before lower state (OVERRIDE_SOFT)
            return o2.overrideState - o1.overrideState;
        }
        if (o1.getSourceUid() == o2.getSourceUid()) {
            final boolean o1FGJ = o1.isRequestedForegroundJob();
            if (o1FGJ != o2.isRequestedForegroundJob()) {
                // Attempt to run requested HPJs ahead of regular jobs, regardless of HPJ quota.
                return o1FGJ ? -1 : 1;
            }
        }
        if (o1.enqueueTime < o2.enqueueTime) {
            return -1;
        }
@@ -1136,6 +1143,12 @@ public class JobSchedulerService extends com.android.server.SystemService

            JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);

            // Return failure early if HPJ quota used up.
            if (jobStatus.isRequestedForegroundJob()
                    && !mQuotaController.isWithinHpjQuotaLocked(jobStatus)) {
                return JobScheduler.RESULT_FAILURE;
            }

            // Give exemption if the source is in the foreground just now.
            // Note if it's a sync job, this method is called on the handler so it's not exactly
            // the state when requestSync() was called, but that should be fine because of the
@@ -1791,9 +1804,9 @@ public class JobSchedulerService extends com.android.server.SystemService
     * time of the job to be the time of completion (i.e. the time at which this function is
     * called).
     * <p>This could be inaccurate b/c the job can run for as long as
     * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
     * to underscheduling at least, rather than if we had taken the last execution time to be the
     * start of the execution.
     * {@link com.android.server.job.JobServiceContext#DEFAULT_EXECUTING_TIMESLICE_MILLIS}, but
     * will lead to underscheduling at least, rather than if we had taken the last execution time
     * to be the start of the execution.
     *
     * @return A new job representing the execution criteria for this instantiation of the
     * recurring job.
@@ -1884,6 +1897,10 @@ public class JobSchedulerService extends com.android.server.SystemService
            Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
        }

        // Intentionally not checking HPJ quota here. An app can't find out if it's run out of quota
        // when it asks JS to reschedule an HPJ. Instead, the rescheduled HPJ will just be demoted
        // to a regular job if the app has no HPJ quota left.

        // If the job wants to be rescheduled, we first need to make the next upcoming
        // job so we can transfer any appropriate state over from the previous job when
        // we stop it.
@@ -2382,7 +2399,7 @@ public class JobSchedulerService extends com.android.server.SystemService
    public long getMaxJobExecutionTimeMs(JobStatus job) {
        synchronized (mLock) {
            return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job),
                    JobServiceContext.EXECUTING_TIMESLICE_MILLIS);
                    JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS);
        }
    }

@@ -2600,7 +2617,6 @@ public class JobSchedulerService extends com.android.server.SystemService
        // job that runs one of the app's services, as well as verifying that the
        // named service properly requires the BIND_JOB_SERVICE permission
        private void enforceValidJobRequest(int uid, JobInfo job) {
            job.enforceValidity();
            final PackageManager pm = getContext()
                    .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0)
                    .getPackageManager();
@@ -2649,6 +2665,7 @@ public class JobSchedulerService extends com.android.server.SystemService
        }

        private void validateJobFlags(JobInfo job, int callingUid) {
            job.enforceValidity();
            if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
                getContext().enforceCallingOrSelfPermission(
                        android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
+26 −6
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.job;

import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;

import android.app.job.IJobCallback;
@@ -73,7 +74,13 @@ public final class JobServiceContext implements ServiceConnection {

    private static final String TAG = "JobServiceContext";
    /** Amount of time a job is allowed to execute for before being considered timed-out. */
    public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000;  // 10mins.
    public static final long DEFAULT_EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000;  // 10mins.
    /**
     * Amount of time a RESTRICTED HPJ is allowed to execute for before being considered
     * timed-out.
     */
    public static final long DEFAULT_RESTRICTED_HPJ_EXECUTING_TIMESLICE_MILLIS =
            DEFAULT_EXECUTING_TIMESLICE_MILLIS / 2;
    /** Amount of time the JobScheduler waits for the initial service launch+bind. */
    private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000;
    /** Amount of time the JobScheduler will wait for a response from an app for a message. */
@@ -224,7 +231,8 @@ public final class JobServiceContext implements ServiceConnection {
            final JobInfo ji = job.getJob();
            mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
                    ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
                    isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network);
                    isDeadlineExpired, job.shouldTreatAsForegroundJob(),
                    triggeredUris, triggeredAuthorities, job.network);
            mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();

            final long whenDeferred = job.getWhenStandbyDeferred();
@@ -250,9 +258,18 @@ public final class JobServiceContext implements ServiceConnection {
            final Intent intent = new Intent().setComponent(job.getServiceComponent());
            boolean binding = false;
            try {
                binding = mContext.bindServiceAsUser(intent, this,
                        Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
                        | Context.BIND_NOT_PERCEPTIBLE,
                final int bindFlags;
                if (job.shouldTreatAsForegroundJob()) {
                    // Add BIND_FOREGROUND_SERVICE to make it BFGS. Without it, it'll be
                    // PROCESS_STATE_IMPORTANT_FOREGROUND. Unclear which is better here.
                    // TODO(171305774): The job should run on the little cores. We'll probably need
                    // another binding flag for that.
                    bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
                } else {
                    bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
                            | Context.BIND_NOT_PERCEPTIBLE;
                }
                binding = mContext.bindServiceAsUser(intent, this, bindFlags,
                        UserHandle.of(job.getUserId()));
            } catch (SecurityException e) {
                // Some permission policy, for example INTERACT_ACROSS_USERS and
@@ -848,7 +865,10 @@ public final class JobServiceContext implements ServiceConnection {
        final long timeoutMillis;
        switch (mVerb) {
            case VERB_EXECUTING:
                timeoutMillis = EXECUTING_TIMESLICE_MILLIS;
                timeoutMillis = mRunningJob.shouldTreatAsForegroundJob()
                        && mRunningJob.getStandbyBucket() == RESTRICTED_INDEX
                        ? DEFAULT_RESTRICTED_HPJ_EXECUTING_TIMESLICE_MILLIS
                        : DEFAULT_EXECUTING_TIMESLICE_MILLIS;
                break;

            case VERB_BINDING:
Loading