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

Commit 22d9f2c3 authored by Kweku Adams's avatar Kweku Adams
Browse files

Add cumulative execution limit for UI jobs.

Add a limit on the total execution time (across multiple execution
instances) of a user-initiated job so that they can't be used
as a pseudo-indefinite keep-alive mechanism. For now, we set the limit
to 24 hours after which the job will be downgraded to a regular job.

Bug: 261999509
Bug: 269139553
Test: atest FrameworksMockingServicesTests:ConnectivityControllerTest
Test: atest FrameworksMockingServicesTests:FlexibilityControllerTest
Test: atest FrameworksMockingServicesTests:JobSchedulerServiceTest
Test: atest FrameworksMockingServicesTests:JobStatusTest
Test: atest FrameworksServicesTests:JobStoreTest
Change-Id: Ie14eb3755e1daae050973f81661ba50a05759252
parent 2b03be6c
Loading
Loading
Loading
Loading
+27 −3
Original line number Diff line number Diff line
@@ -483,6 +483,7 @@ public class JobSchedulerService extends com.android.server.SystemService
                        case Constants.KEY_RUNTIME_UI_LIMIT_MS:
                        case Constants.KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR:
                        case Constants.KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS:
                        case Constants.KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS:
                        case Constants.KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS:
                            if (!runtimeUpdated) {
                                mConstants.updateRuntimeConstantsLocked();
@@ -583,6 +584,8 @@ public class JobSchedulerService extends com.android.server.SystemService
                "runtime_min_ui_data_transfer_guarantee_buffer_factor";
        private static final String KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS =
                "runtime_min_ui_data_transfer_guarantee_ms";
        private static final String KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS =
                "runtime_cumulative_ui_limit_ms";
        private static final String KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS =
                "runtime_use_data_estimates_for_limits";

@@ -623,6 +626,7 @@ public class JobSchedulerService extends com.android.server.SystemService
                1.35f;
        public static final long DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS =
                Math.max(10 * MINUTE_IN_MILLIS, DEFAULT_RUNTIME_MIN_UI_GUARANTEE_MS);
        public static final long DEFAULT_RUNTIME_CUMULATIVE_UI_LIMIT_MS = 24 * HOUR_IN_MILLIS;
        public static final boolean DEFAULT_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = false;
        static final boolean DEFAULT_PERSIST_IN_SPLIT_FILES = true;
        static final int DEFAULT_MAX_NUM_PERSISTED_JOB_WORK_ITEMS = 100_000;
@@ -760,6 +764,12 @@ public class JobSchedulerService extends com.android.server.SystemService
        public long RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS =
                DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS;

        /**
         * The maximum amount of cumulative time we will let a user-initiated job run for
         * before downgrading it.
         */
        public long RUNTIME_CUMULATIVE_UI_LIMIT_MS = DEFAULT_RUNTIME_CUMULATIVE_UI_LIMIT_MS;

        /**
         * Whether to use data estimates to determine execution limits for execution limits.
         */
@@ -882,6 +892,7 @@ public class JobSchedulerService extends com.android.server.SystemService
                    KEY_RUNTIME_MIN_UI_GUARANTEE_MS,
                    KEY_RUNTIME_UI_LIMIT_MS,
                    KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS,
                    KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS,
                    KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS);

            // Make sure min runtime for regular jobs is at least 10 minutes.
@@ -916,6 +927,11 @@ public class JobSchedulerService extends com.android.server.SystemService
                    properties.getLong(
                            KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS,
                            DEFAULT_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS));
            // The cumulative runtime limit should be at least the max execution limit.
            RUNTIME_CUMULATIVE_UI_LIMIT_MS = Math.max(RUNTIME_UI_LIMIT_MS,
                    properties.getLong(
                            KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS,
                            DEFAULT_RUNTIME_CUMULATIVE_UI_LIMIT_MS));

            RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS = properties.getBoolean(
                    KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS,
@@ -973,6 +989,7 @@ public class JobSchedulerService extends com.android.server.SystemService
                    RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR).println();
            pw.print(KEY_RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS,
                    RUNTIME_MIN_UI_DATA_TRANSFER_GUARANTEE_MS).println();
            pw.print(KEY_RUNTIME_CUMULATIVE_UI_LIMIT_MS, RUNTIME_CUMULATIVE_UI_LIMIT_MS).println();
            pw.print(KEY_RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS,
                    RUNTIME_USE_DATA_ESTIMATES_FOR_LIMITS).println();

@@ -2447,11 +2464,16 @@ public class JobSchedulerService extends com.android.server.SystemService
        JobStatus newJob = new JobStatus(failureToReschedule,
                elapsedNowMillis + delayMillis,
                JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops,
                failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis());
                failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis(),
                failureToReschedule.getCumulativeExecutionTimeMs());
        if (stopReason == JobParameters.STOP_REASON_USER) {
            // Demote all jobs to regular for user stops so they don't keep privileges.
            newJob.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
        }
        if (newJob.getCumulativeExecutionTimeMs() >= mConstants.RUNTIME_CUMULATIVE_UI_LIMIT_MS
                && newJob.shouldTreatAsUserInitiatedJob()) {
            newJob.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ);
        }
        if (job.isPeriodic()) {
            newJob.setOriginalLatestRunTimeElapsed(
                    failureToReschedule.getOriginalLatestRunTimeElapsed());
@@ -2543,7 +2565,8 @@ public class JobSchedulerService extends com.android.server.SystemService
                    elapsedNow + period - flex, elapsedNow + period,
                    0 /* numFailures */, 0 /* numSystemStops */,
                    sSystemClock.millis() /* lastSuccessfulRunTime */,
                    periodicToReschedule.getLastFailedRunTime());
                    periodicToReschedule.getLastFailedRunTime(),
                    0 /* Reset cumulativeExecutionTime because of successful execution */);
        }

        final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed
@@ -2558,7 +2581,8 @@ public class JobSchedulerService extends com.android.server.SystemService
                newEarliestRunTimeElapsed, newLatestRuntimeElapsed,
                0 /* numFailures */, 0 /* numSystemStops */,
                sSystemClock.millis() /* lastSuccessfulRunTime */,
                periodicToReschedule.getLastFailedRunTime());
                periodicToReschedule.getLastFailedRunTime(),
                0 /* Reset cumulativeExecutionTime because of successful execution */);
    }

    // JobCompletedListener implementations.
+18 −2
Original line number Diff line number Diff line
@@ -109,6 +109,7 @@ public final class JobServiceContext implements ServiceConnection {
    private static final long OP_TIMEOUT_MILLIS = 8 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
    /** Amount of time the JobScheduler will wait for a job to provide a required notification. */
    private static final long NOTIFICATION_TIMEOUT_MILLIS = 10_000L * Build.HW_TIMEOUT_MULTIPLIER;
    private static final long EXECUTION_DURATION_STAMP_PERIOD_MILLIS = 5 * 60_000L;

    private static final String[] VERB_STRINGS = {
            "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
@@ -194,6 +195,8 @@ public final class JobServiceContext implements ServiceConnection {
    private long mMaxExecutionTimeMillis;
    /** Whether this job is required to provide a notification and we're still waiting for it. */
    private boolean mAwaitingNotification;
    /** The last time we updated the job's execution duration, in the elapsed realtime timebase. */
    private long mLastExecutionDurationStampTimeElapsed;

    private long mEstimatedDownloadBytes;
    private long mEstimatedUploadBytes;
@@ -1234,7 +1237,15 @@ public final class JobServiceContext implements ServiceConnection {
                            /* anrMessage */ "required notification not provided",
                            /* triggerAnr */ true);
                } else {
                    final long timeSinceDurationStampTimeMs =
                            nowElapsed - mLastExecutionDurationStampTimeElapsed;
                    if (timeSinceDurationStampTimeMs < EXECUTION_DURATION_STAMP_PERIOD_MILLIS) {
                        Slog.e(TAG, "Unexpected op timeout while EXECUTING");
                    }
                    // Update the execution time even if this wasn't the pre-set time.
                    mRunningJob.incrementCumulativeExecutionTime(timeSinceDurationStampTimeMs);
                    mService.mJobs.touchJob(mRunningJob);
                    mLastExecutionDurationStampTimeElapsed = nowElapsed;
                    scheduleOpTimeOutLocked();
                }
                break;
@@ -1303,8 +1314,11 @@ public final class JobServiceContext implements ServiceConnection {
            Slog.d(TAG, "Cleaning up " + mRunningJob.toShortString()
                    + " reschedule=" + reschedule + " reason=" + loggingDebugReason);
        }
        final long nowElapsed = sElapsedRealtimeClock.millis();
        applyStoppedReasonLocked(loggingDebugReason);
        completedJob = mRunningJob;
        completedJob.incrementCumulativeExecutionTime(
                nowElapsed - mLastExecutionDurationStampTimeElapsed);
        // Use the JobParameters stop reasons for logging and metric purposes,
        // but if the job was marked for death, use that reason for rescheduling purposes.
        // The discrepancy could happen if a job ends up stopping for some reason
@@ -1331,7 +1345,7 @@ public final class JobServiceContext implements ServiceConnection {
        mPreviousJobHadSuccessfulFinish =
                (loggingInternalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
        if (!mPreviousJobHadSuccessfulFinish) {
            mLastUnsuccessfulFinishElapsed = sElapsedRealtimeClock.millis();
            mLastUnsuccessfulFinishElapsed = nowElapsed;
        }
        mJobPackageTracker.noteInactive(completedJob,
                loggingInternalStopReason, loggingDebugReason);
@@ -1400,6 +1414,7 @@ public final class JobServiceContext implements ServiceConnection {
        mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
        mDeathMarkInternalStopReason = 0;
        mDeathMarkDebugReason = null;
        mLastExecutionDurationStampTimeElapsed = 0;
        mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
        mPendingInternalStopReason = 0;
        mPendingDebugStopReason = null;
@@ -1449,6 +1464,7 @@ public final class JobServiceContext implements ServiceConnection {
                if (mAwaitingNotification) {
                    minTimeout = Math.min(minTimeout, NOTIFICATION_TIMEOUT_MILLIS);
                }
                minTimeout = Math.min(minTimeout, EXECUTION_DURATION_STAMP_PERIOD_MILLIS);
                timeoutMillis = minTimeout;
                break;

+11 −3
Original line number Diff line number Diff line
@@ -236,7 +236,8 @@ public final class JobStore {
                        convertRtcBoundsToElapsed(utcTimes, elapsedNow);
                JobStatus newJob = new JobStatus(job,
                        elapsedRuntimes.first, elapsedRuntimes.second,
                        0, 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime());
                        0, 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime(),
                        job.getCumulativeExecutionTimeMs());
                newJob.prepareLocked();
                toAdd.add(newJob);
                toRemove.add(job);
@@ -786,7 +787,7 @@ public final class JobStore {
         * Write out a tag with data comprising the required fields and bias of this job and
         * its client.
         */
        private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
        private void addAttributesToJobTag(TypedXmlSerializer out, JobStatus jobStatus)
                throws IOException {
            out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
            out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
@@ -813,6 +814,9 @@ public final class JobStore {
                    String.valueOf(jobStatus.getLastSuccessfulRunTime()));
            out.attribute(null, "lastFailedRunTime",
                    String.valueOf(jobStatus.getLastFailedRunTime()));

            out.attributeLong(null, "cumulativeExecutionTime",
                    jobStatus.getCumulativeExecutionTimeMs());
        }

        private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
@@ -1190,6 +1194,7 @@ public final class JobStore {
            int uid, sourceUserId;
            long lastSuccessfulRunTime;
            long lastFailedRunTime;
            long cumulativeExecutionTime;
            int internalFlags = 0;

            // Read out job identifier attributes and bias.
@@ -1230,6 +1235,9 @@ public final class JobStore {

                val = parser.getAttributeValue(null, "lastFailedRunTime");
                lastFailedRunTime = val == null ? 0 : Long.parseLong(val);

                cumulativeExecutionTime =
                        parser.getAttributeLong(null, "cumulativeExecutionTime", 0);
            } catch (NumberFormatException e) {
                Slog.e(TAG, "Error parsing job's required fields, skipping");
                return null;
@@ -1402,7 +1410,7 @@ public final class JobStore {
                    builtJob, uid, sourcePackageName, sourceUserId,
                    appBucket, namespace, sourceTag,
                    elapsedRuntimes.first, elapsedRuntimes.second,
                    lastSuccessfulRunTime, lastFailedRunTime,
                    lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTime,
                    (rtcIsGood) ? null : rtcRuntimes, internalFlags, /* dynamicConstraints */ 0);
            if (jobWorkItems != null) {
                for (int i = 0; i < jobWorkItems.size(); ++i) {
+39 −5
Original line number Diff line number Diff line
@@ -375,6 +375,11 @@ public final class JobStatus {
     * and is thus considered demoted from whatever privileged state it had in the past.
     */
    public static final int INTERNAL_FLAG_DEMOTED_BY_USER = 1 << 1;
    /**
     * Flag for {@link #mInternalFlags}: this job is demoted by the system
     * from running as a user-initiated job.
     */
    public static final int INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ = 1 << 2;

    /** Minimum difference between start and end time to have flexible constraint */
    @VisibleForTesting
@@ -385,6 +390,12 @@ public final class JobStatus {
     */
    private int mInternalFlags;

    /**
     * The cumulative amount of time this job has run for, including previous executions.
     * This is reset for periodic jobs upon a successful job execution.
     */
    private long mCumulativeExecutionTimeMs;

    // These are filled in by controllers when preparing for execution.
    public ArraySet<Uri> changedUris;
    public ArraySet<String> changedAuthorities;
@@ -550,7 +561,8 @@ public final class JobStatus {
            int sourceUserId, int standbyBucket, @Nullable String namespace, String tag,
            int numFailures, int numSystemStops,
            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
            long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags,
            long lastSuccessfulRunTime, long lastFailedRunTime, long cumulativeExecutionTimeMs,
            int internalFlags,
            int dynamicConstraints) {
        this.job = job;
        this.callingUid = callingUid;
@@ -650,6 +662,8 @@ public final class JobStatus {
            mReadyDynamicSatisfied = false;
        }

        mCumulativeExecutionTimeMs = cumulativeExecutionTimeMs;

        mLastSuccessfulRunTime = lastSuccessfulRunTime;
        mLastFailedRunTime = lastFailedRunTime;

@@ -684,6 +698,7 @@ public final class JobStatus {
                jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(),
                jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
                jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
                jobStatus.getCumulativeExecutionTimeMs(),
                jobStatus.getInternalFlags(), jobStatus.mDynamicConstraints);
        mPersistedUtcTimes = jobStatus.mPersistedUtcTimes;
        if (jobStatus.mPersistedUtcTimes != null) {
@@ -711,13 +726,15 @@ public final class JobStatus {
            int standbyBucket, @Nullable String namespace, String sourceTag,
            long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
            long lastSuccessfulRunTime, long lastFailedRunTime,
            long cumulativeExecutionTimeMs,
            Pair<Long, Long> persistedExecutionTimesUTC,
            int innerFlags, int dynamicConstraints) {
        this(job, callingUid, sourcePkgName, sourceUserId,
                standbyBucket, namespace,
                sourceTag, /* numFailures */ 0, /* numSystemStops */ 0,
                earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                lastSuccessfulRunTime, lastFailedRunTime, innerFlags, dynamicConstraints);
                lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTimeMs,
                innerFlags, dynamicConstraints);

        // Only during initial inflation do we record the UTC-timebase execution bounds
        // read from the persistent store.  If we ever have to recreate the JobStatus on
@@ -735,14 +752,16 @@ public final class JobStatus {
    public JobStatus(JobStatus rescheduling,
            long newEarliestRuntimeElapsedMillis,
            long newLatestRuntimeElapsedMillis, int numFailures, int numSystemStops,
            long lastSuccessfulRunTime, long lastFailedRunTime) {
            long lastSuccessfulRunTime, long lastFailedRunTime,
            long cumulativeExecutionTimeMs) {
        this(rescheduling.job, rescheduling.getUid(),
                rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
                rescheduling.getStandbyBucket(), rescheduling.getNamespace(),
                rescheduling.getSourceTag(), numFailures, numSystemStops,
                newEarliestRuntimeElapsedMillis,
                newLatestRuntimeElapsedMillis,
                lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags(),
                lastSuccessfulRunTime, lastFailedRunTime, cumulativeExecutionTimeMs,
                rescheduling.getInternalFlags(),
                rescheduling.mDynamicConstraints);
    }

@@ -780,6 +799,7 @@ public final class JobStatus {
                standbyBucket, namespace, tag, /* numFailures */ 0, /* numSystemStops */ 0,
                earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
                /* cumulativeExecutionTime */ 0,
                /*innerFlags=*/ 0, /* dynamicConstraints */ 0);
    }

@@ -1312,6 +1332,14 @@ public final class JobStatus {
        return job.isPersisted();
    }

    public long getCumulativeExecutionTimeMs() {
        return mCumulativeExecutionTimeMs;
    }

    public void incrementCumulativeExecutionTime(long incrementMs) {
        mCumulativeExecutionTimeMs += incrementMs;
    }

    public long getEarliestRunTime() {
        return earliestRunTimeElapsedMillis;
    }
@@ -1405,7 +1433,8 @@ public final class JobStatus {
     */
    public boolean shouldTreatAsUserInitiatedJob() {
        return getJob().isUserInitiated()
                && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_USER) == 0;
                && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_USER) == 0
                && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ) == 0;
    }

    /**
@@ -2655,6 +2684,11 @@ public final class JobStatus {
        pw.print(", original latest=");
        formatRunTime(pw, mOriginalLatestRunTimeElapsedMillis, NO_LATEST_RUNTIME, nowElapsed);
        pw.println();
        if (mCumulativeExecutionTimeMs != 0) {
            pw.print("Cumulative execution time=");
            TimeUtils.formatDuration(mCumulativeExecutionTimeMs, pw);
            pw.println();
        }
        if (numFailures != 0) {
            pw.print("Num failures: "); pw.println(numFailures);
        }
+44 −0
Original line number Diff line number Diff line
@@ -337,6 +337,50 @@ public class JobSchedulerServiceTest {
                mService.getMaxJobExecutionTimeMs(jobUIDT));
    }

    /**
     * Confirm that
     * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)}
     * returns a job that is no longer allowed to run as a user-initiated job after it hits
     * the cumulative execution limit.
     */
    @Test
    public void testGetRescheduleJobForFailure_cumulativeExecution() {
        JobStatus originalJob = createJobStatus("testGetRescheduleJobForFailure",
                createJobInfo()
                        .setUserInitiated(true)
                        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY));
        assertTrue(originalJob.shouldTreatAsUserInitiatedJob());

        // Cumulative time = 0
        JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob,
                JobParameters.STOP_REASON_UNDEFINED,
                JobParameters.INTERNAL_STOP_REASON_UNKNOWN);
        assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob());

        // Cumulative time = 50% of limit
        rescheduledJob.incrementCumulativeExecutionTime(
                mService.mConstants.RUNTIME_CUMULATIVE_UI_LIMIT_MS / 2);
        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
                JobParameters.STOP_REASON_UNDEFINED,
                JobParameters.INTERNAL_STOP_REASON_UNKNOWN);
        assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob());

        // Cumulative time = 99.999999% of limit
        rescheduledJob.incrementCumulativeExecutionTime(
                mService.mConstants.RUNTIME_CUMULATIVE_UI_LIMIT_MS / 2 - 1);
        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
                JobParameters.STOP_REASON_UNDEFINED,
                JobParameters.INTERNAL_STOP_REASON_UNKNOWN);
        assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob());

        // Cumulative time = 100+% of limit
        rescheduledJob.incrementCumulativeExecutionTime(2);
        rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob,
                JobParameters.STOP_REASON_UNDEFINED,
                JobParameters.INTERNAL_STOP_REASON_UNKNOWN);
        assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob());
    }

    /**
     * Confirm that
     * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)}
Loading