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

Commit 57c9f5ce authored by Kweku Adams's avatar Kweku Adams
Browse files

Add an API to query pending job reason.

With Data Transfer jobs, an app may want to tell the user why the
transfer isn't running. This adds a simple API that lets the app
query why a job may isn't running. It will only return one reason even
if there are multiple reasons the job isn't running.

Bug: 255371817
Test: atest CtsJobSchedulerTestCases:JobSchedulingTest
Change-Id: Ie86079c63a94312007406a43c4883e18d39a00ab
parent 9cba04b0
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -108,6 +108,15 @@ public class JobSchedulerImpl extends JobScheduler {
        }
    }

    @Override
    public int getPendingJobReason(int jobId) {
        try {
            return mBinder.getPendingJobReason(jobId);
        } catch (RemoteException e) {
            return PENDING_JOB_REASON_UNDEFINED;
        }
    }

    @Override
    public boolean canRunLongJobs() {
        try {
+1 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ interface IJobScheduler {
    void cancelAll();
    ParceledListSlice getAllPendingJobs();
    JobInfo getPendingJob(int jobId);
    int getPendingJobReason(int jobId);
    boolean canRunLongJobs(String packageName);
    boolean hasRunLongJobsPermission(String packageName, int userId);
    List<JobInfo> getStartedJobs();
+139 −0
Original line number Diff line number Diff line
@@ -23,10 +23,14 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.usage.UsageStatsManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.ClipData;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.NetworkRequest;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
@@ -133,6 +137,132 @@ public abstract class JobScheduler {
     */
    public static final int RESULT_SUCCESS = 1;

    /** The job doesn't exist. */
    public static final int PENDING_JOB_REASON_INVALID_JOB_ID = -2;
    /** The job is currently running and is therefore not pending. */
    public static final int PENDING_JOB_REASON_EXECUTING = -1;
    /**
     * There is no known reason why the job is pending.
     * If additional reasons are added on newer Android versions, the system may return this reason
     * to apps whose target SDK is not high enough to expect that reason.
     */
    public static final int PENDING_JOB_REASON_UNDEFINED = 0;
    /**
     * The app is in a state that prevents the job from running
     * (eg. the {@link JobService} component is disabled).
     */
    public static final int PENDING_JOB_REASON_APP = 1;
    /**
     * The current standby bucket prevents the job from running.
     *
     * @see UsageStatsManager#STANDBY_BUCKET_RESTRICTED
     */
    public static final int PENDING_JOB_REASON_APP_STANDBY = 2;
    /**
     * The app is restricted from running in the background.
     *
     * @see ActivityManager#isBackgroundRestricted()
     * @see PackageManager#isInstantApp()
     */
    public static final int PENDING_JOB_REASON_BACKGROUND_RESTRICTION = 3;
    /**
     * The requested battery-not-low constraint is not satisfied.
     *
     * @see JobInfo.Builder#setRequiresBatteryNotLow(boolean)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW = 4;
    /**
     * The requested charging constraint is not satisfied.
     *
     * @see JobInfo.Builder#setRequiresCharging(boolean)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_CHARGING = 5;
    /**
     * The requested connectivity constraint is not satisfied.
     *
     * @see JobInfo.Builder#setRequiredNetwork(NetworkRequest)
     * @see JobInfo.Builder#setRequiredNetworkType(int)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY = 6;
    /**
     * The requested content trigger constraint is not satisfied.
     *
     * @see JobInfo.Builder#addTriggerContentUri(JobInfo.TriggerContentUri)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER = 7;
    /**
     * The requested idle constraint is not satisfied.
     *
     * @see JobInfo.Builder#setRequiresDeviceIdle(boolean)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE = 8;
    /**
     * The minimum latency has not transpired.
     *
     * @see JobInfo.Builder#setMinimumLatency(long)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY = 9;
    /**
     * The system's estimate of when the app will be launched is far away enough to warrant delaying
     * this job.
     *
     * @see JobInfo#isPrefetch()
     * @see JobInfo.Builder#setPrefetch(boolean)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_PREFETCH = 10;
    /**
     * The requested storage-not-low constraint is not satisfied.
     *
     * @see JobInfo.Builder#setRequiresStorageNotLow(boolean)
     */
    public static final int PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW = 11;
    /**
     * The job is being deferred due to the device state (eg. Doze, battery saver, memory usage,
     * thermal status, etc.).
     */
    public static final int PENDING_JOB_REASON_DEVICE_STATE = 12;
    /**
     * JobScheduler thinks it can defer this job to a more optimal running time.
     */
    public static final int PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION = 13;
    /**
     * The app has consumed all of its current quota.
     *
     * @see UsageStatsManager#getAppStandbyBucket()
     * @see JobParameters#STOP_REASON_QUOTA
     */
    public static final int PENDING_JOB_REASON_QUOTA = 14;
    /**
     * JobScheduler is respecting one of the user's actions (eg. force stop or adb shell commands)
     * to defer this job.
     */
    public static final int PENDING_JOB_REASON_USER = 15;

    /** @hide */
    @IntDef(prefix = {"PENDING_JOB_REASON_"}, value = {
            PENDING_JOB_REASON_UNDEFINED,
            PENDING_JOB_REASON_APP,
            PENDING_JOB_REASON_APP_STANDBY,
            PENDING_JOB_REASON_BACKGROUND_RESTRICTION,
            PENDING_JOB_REASON_CONSTRAINT_BATTERY_NOT_LOW,
            PENDING_JOB_REASON_CONSTRAINT_CHARGING,
            PENDING_JOB_REASON_CONSTRAINT_CONNECTIVITY,
            PENDING_JOB_REASON_CONSTRAINT_CONTENT_TRIGGER,
            PENDING_JOB_REASON_CONSTRAINT_DEVICE_IDLE,
            PENDING_JOB_REASON_CONSTRAINT_MINIMUM_LATENCY,
            PENDING_JOB_REASON_CONSTRAINT_PREFETCH,
            PENDING_JOB_REASON_CONSTRAINT_STORAGE_NOT_LOW,
            PENDING_JOB_REASON_DEVICE_STATE,
            PENDING_JOB_REASON_EXECUTING,
            PENDING_JOB_REASON_INVALID_JOB_ID,
            PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION,
            PENDING_JOB_REASON_QUOTA,
            PENDING_JOB_REASON_USER,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PendingJobReason {
    }

    /**
     * Schedule a job to be executed.  Will replace any currently scheduled job with the same
     * ID with the new information in the {@link JobInfo}.  If a job with the given ID is currently
@@ -249,6 +379,15 @@ public abstract class JobScheduler {
     */
    public abstract @Nullable JobInfo getPendingJob(int jobId);

    /**
     * Returns a reason why the job is pending and not currently executing. If there are multiple
     * reasons why a job may be pending, this will only return one of them.
     */
    @PendingJobReason
    public int getPendingJobReason(int jobId) {
        return PENDING_JOB_REASON_UNDEFINED;
    }

    /**
     * Returns {@code true} if the calling app currently holds the
     * {@link android.Manifest.permission#RUN_LONG_JOBS} permission, allowing it to run long jobs.
+3 −2
Original line number Diff line number Diff line
@@ -1175,7 +1175,7 @@ class JobConcurrencyManager {

            if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
                    && restriction.isJobRestricted(jobStatus)) {
                jsc.cancelExecutingJobLocked(restriction.getReason(),
                jsc.cancelExecutingJobLocked(restriction.getStopReason(),
                        restriction.getInternalReason(),
                        JobParameters.getInternalReasonCodeDescription(
                                restriction.getInternalReason()));
@@ -1208,7 +1208,7 @@ class JobConcurrencyManager {
                final JobRestriction restriction = mService.checkIfRestricted(running);
                if (restriction != null) {
                    final int internalReasonCode = restriction.getInternalReason();
                    serviceContext.cancelExecutingJobLocked(restriction.getReason(),
                    serviceContext.cancelExecutingJobLocked(restriction.getStopReason(),
                            internalReasonCode,
                            "restricted due to "
                                    + JobParameters.getInternalReasonCodeDescription(
@@ -1324,6 +1324,7 @@ class JobConcurrencyManager {
                mActivePkgStats.add(
                        jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(),
                        packageStats);
                mService.resetPendingJobReasonCache(jobStatus);
            }
            if (mService.getPendingJobQueue().remove(jobStatus)) {
                mService.mJobPackageTracker.noteNonpending(jobStatus);
+191 −0
Original line number Diff line number Diff line
@@ -363,6 +363,9 @@ public class JobSchedulerService extends com.android.server.SystemService
    @GuardedBy("mLock")
    private final ArraySet<JobStatus> mChangedJobList = new ArraySet<>();

    @GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing
    private final SparseArray<SparseIntArray> mPendingJobReasonCache = new SparseArray<>();

    /**
     * Named indices into standby bucket arrays, for clarity in referring to
     * specific buckets' bookkeeping.
@@ -1357,6 +1360,134 @@ public class JobSchedulerService extends com.android.server.SystemService
        }
    }

    @JobScheduler.PendingJobReason
    private int getPendingJobReason(int uid, int jobId) {
        int reason;
        // Some apps may attempt to query this frequently, so cache the reason under a separate lock
        // so that the rest of JS processing isn't negatively impacted.
        synchronized (mPendingJobReasonCache) {
            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
            if (jobIdToReason != null) {
                reason = jobIdToReason.get(jobId, JobScheduler.PENDING_JOB_REASON_UNDEFINED);
                if (reason != JobScheduler.PENDING_JOB_REASON_UNDEFINED) {
                    return reason;
                }
            }
        }
        synchronized (mLock) {
            reason = getPendingJobReasonLocked(uid, jobId);
            if (DEBUG) {
                Slog.v(TAG, "getPendingJobReason(" + uid + "," + jobId + ")=" + reason);
            }
        }
        synchronized (mPendingJobReasonCache) {
            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
            if (jobIdToReason == null) {
                jobIdToReason = new SparseIntArray();
                mPendingJobReasonCache.put(uid, jobIdToReason);
            }
            jobIdToReason.put(jobId, reason);
        }
        return reason;
    }

    @JobScheduler.PendingJobReason
    @GuardedBy("mLock")
    private int getPendingJobReasonLocked(int uid, int jobId) {
        // Very similar code to isReadyToBeExecutedLocked.

        JobStatus job = mJobs.getJobByUidAndJobId(uid, jobId);
        if (job == null) {
            // Job doesn't exist.
            return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID;
        }

        if (isCurrentlyRunningLocked(job)) {
            return JobScheduler.PENDING_JOB_REASON_EXECUTING;
        }

        final boolean jobReady = job.isReady();

        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " ready=" + jobReady);
        }

        if (!jobReady) {
            return job.getPendingJobReason();
        }

        final boolean userStarted = areUsersStartedLocked(job);

        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " userStarted=" + userStarted);
        }
        if (!userStarted) {
            return JobScheduler.PENDING_JOB_REASON_USER;
        }

        final boolean backingUp = mBackingUpUids.get(job.getSourceUid());
        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " backingUp=" + backingUp);
        }

        if (backingUp) {
            // TODO: Should we make a special reason for this?
            return JobScheduler.PENDING_JOB_REASON_APP;
        }

        JobRestriction restriction = checkIfRestricted(job);
        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " restriction=" + restriction);
        }
        if (restriction != null) {
            return restriction.getPendingReason();
        }

        // The following can be a little more expensive (especially jobActive, since we need to
        // go through the array of all potentially active jobs), so we are doing them
        // later...  but still before checking with the package manager!
        final boolean jobPending = mPendingJobQueue.contains(job);


        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " pending=" + jobPending);
        }

        if (jobPending) {
            // We haven't started the job for some reason. Presumably, there are too many jobs
            // running.
            return JobScheduler.PENDING_JOB_REASON_DEVICE_STATE;
        }

        final boolean jobActive = mConcurrencyManager.isJobRunningLocked(job);

        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " active=" + jobActive);
        }
        if (jobActive) {
            return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
        }

        // Validate that the defined package+service is still present & viable.
        final boolean componentUsable = isComponentUsable(job);

        if (DEBUG) {
            Slog.v(TAG, "getPendingJobReasonLocked: " + job.toShortString()
                    + " componentUsable=" + componentUsable);
        }
        if (!componentUsable) {
            return JobScheduler.PENDING_JOB_REASON_APP;
        }

        return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
    }

    public JobInfo getPendingJob(int uid, int jobId) {
        synchronized (mLock) {
            ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
@@ -1389,6 +1520,9 @@ public class JobSchedulerService extends com.android.server.SystemService
        synchronized (mLock) {
            mJobs.removeJobsOfUnlistedUsers(umi.getUserIds());
        }
        synchronized (mPendingJobReasonCache) {
            mPendingJobReasonCache.clear();
        }
    }

    private void cancelJobsForPackageAndUidLocked(String pkgName, int uid,
@@ -1874,6 +2008,8 @@ public class JobSchedulerService extends com.android.server.SystemService
        jobStatus.enqueueTime = sElapsedRealtimeClock.millis();
        final boolean update = lastJob != null;
        mJobs.add(jobStatus);
        // Clear potentially cached INVALID_JOB_ID reason.
        resetPendingJobReasonCache(jobStatus);
        if (mReadyToRock) {
            for (int i = 0; i < mControllers.size(); i++) {
                StateController controller = mControllers.get(i);
@@ -1895,6 +2031,13 @@ public class JobSchedulerService extends com.android.server.SystemService
        // Deal with any remaining work items in the old job.
        jobStatus.stopTrackingJobLocked(incomingJob);

        synchronized (mPendingJobReasonCache) {
            SparseIntArray reasonCache = mPendingJobReasonCache.get(jobStatus.getUid());
            if (reasonCache != null) {
                reasonCache.delete(jobStatus.getJobId());
            }
        }

        // Remove from store as well as controllers.
        final boolean removed = mJobs.remove(jobStatus, removeFromPersisted);
        if (!removed) {
@@ -1917,6 +2060,16 @@ public class JobSchedulerService extends com.android.server.SystemService
        return removed;
    }

    /** Remove the pending job reason for this job from the cache. */
    void resetPendingJobReasonCache(@NonNull JobStatus jobStatus) {
        synchronized (mPendingJobReasonCache) {
            final SparseIntArray reasons = mPendingJobReasonCache.get(jobStatus.getUid());
            if (reasons != null) {
                reasons.delete(jobStatus.getJobId());
            }
        }
    }

    /** Return {@code true} if the specified job is currently executing. */
    @GuardedBy("mLock")
    public boolean isCurrentlyRunningLocked(JobStatus job) {
@@ -2210,11 +2363,20 @@ public class JobSchedulerService extends com.android.server.SystemService
    public void onControllerStateChanged(@Nullable ArraySet<JobStatus> changedJobs) {
        if (changedJobs == null) {
            mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
            synchronized (mPendingJobReasonCache) {
                mPendingJobReasonCache.clear();
            }
        } else if (changedJobs.size() > 0) {
            synchronized (mLock) {
                mChangedJobList.addAll(changedJobs);
            }
            mHandler.obtainMessage(MSG_CHECK_CHANGED_JOB_LIST).sendToTarget();
            synchronized (mPendingJobReasonCache) {
                for (int i = changedJobs.size() - 1; i >= 0; --i) {
                    final JobStatus job = changedJobs.valueAt(i);
                    resetPendingJobReasonCache(job);
                }
            }
        }
    }

@@ -2568,6 +2730,23 @@ public class JobSchedulerService extends com.android.server.SystemService
                if (DEBUG) {
                    Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything.");
                }
                final int numRunnableJobs = runnableJobs.size();
                if (numRunnableJobs > 0) {
                    synchronized (mPendingJobReasonCache) {
                        for (int i = 0; i < numRunnableJobs; ++i) {
                            final JobStatus job = runnableJobs.get(i);
                            SparseIntArray reasons = mPendingJobReasonCache.get(job.getUid());
                            if (reasons == null) {
                                reasons = new SparseIntArray();
                                mPendingJobReasonCache.put(job.getUid(), reasons);
                            }
                            // We're force batching these jobs, so consider it an optimization
                            // policy reason.
                            reasons.put(job.getJobId(),
                                    JobScheduler.PENDING_JOB_REASON_JOB_SCHEDULER_OPTIMIZATION);
                        }
                    }
                }
            }

            // Be ready for next time
@@ -3369,6 +3548,18 @@ public class JobSchedulerService extends com.android.server.SystemService
            }
        }

        @Override
        public int getPendingJobReason(int jobId) throws RemoteException {
            final int uid = Binder.getCallingUid();

            final long ident = Binder.clearCallingIdentity();
            try {
                return JobSchedulerService.this.getPendingJobReason(uid, jobId);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override
        public JobInfo getPendingJob(int jobId) throws RemoteException {
            final int uid = Binder.getCallingUid();
Loading