diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java index 862d8b7cac508..3f46cc44d379c 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java @@ -16,6 +16,8 @@ package com.android.server.job; +import android.app.job.JobParameters; + import com.android.server.job.controllers.JobStatus; /** @@ -26,8 +28,12 @@ public interface JobCompletedListener { /** * Callback for when a job is completed. * - * @param stopReason The stop reason provided to JobParameters. - * @param needsReschedule Whether the implementing class should reschedule this job. + * @param stopReason The stop reason returned from + * {@link JobParameters#getStopReason()}. + * @param internalStopReason The stop reason returned from + * {@link JobParameters#getInternalStopReasonCode()}. + * @param needsReschedule Whether the implementing class should reschedule this job. */ - void onJobCompletedLocked(JobStatus jobStatus, int stopReason, boolean needsReschedule); + void onJobCompletedLocked(JobStatus jobStatus, int stopReason, int internalStopReason, + boolean needsReschedule); } 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 5e4d000b0d78f..299cb6cddbdf1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1695,10 +1695,28 @@ public class JobSchedulerService extends com.android.server.SystemService } private void stopUserVisibleJobsInternal(@NonNull String packageName, int userId) { + final int packageUid = mLocalPM.getPackageUid(packageName, 0, userId); + if (packageUid < 0) { + Slog.wtf(TAG, "Asked to stop jobs of an unknown package"); + return; + } synchronized (mLock) { mConcurrencyManager.stopUserVisibleJobsLocked(userId, packageName, JobParameters.STOP_REASON_USER, JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP); + final ArraySet jobs = mJobs.getJobsByUid(packageUid); + for (int i = jobs.size() - 1; i >= 0; i--) { + final JobStatus job = jobs.valueAt(i); + + // For now, demote all jobs of the app. However, if the app was only doing work + // on behalf of another app and the user wanted just that work to stop, this + // unfairly penalizes any other jobs that may be scheduled. + // For example, if apps A & B ask app C to do something (thus A & B are "source" + // and C is "calling"), but only A's work was under way and the user wanted + // to stop only that work, B's jobs would be demoted as well. + // TODO(255768978): make it possible to demote only the relevant subset of jobs + job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + } } } @@ -2352,7 +2370,7 @@ public class JobSchedulerService extends com.android.server.SystemService */ @VisibleForTesting JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule, - int internalStopReason) { + @JobParameters.StopReason int stopReason, int internalStopReason) { final long elapsedNowMillis = sElapsedRealtimeClock.millis(); final JobInfo job = failureToReschedule.getJob(); @@ -2360,9 +2378,11 @@ public class JobSchedulerService extends com.android.server.SystemService int numFailures = failureToReschedule.getNumFailures(); int numSystemStops = failureToReschedule.getNumSystemStops(); // We should back off slowly if JobScheduler keeps stopping the job, - // but back off immediately if the issue appeared to be the app's fault. + // but back off immediately if the issue appeared to be the app's fault + // or the user stopped the job somehow. if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH - || internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT) { + || internalStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT + || stopReason == JobParameters.STOP_REASON_USER) { numFailures++; } else { numSystemStops++; @@ -2393,11 +2413,14 @@ public class JobSchedulerService extends com.android.server.SystemService } delayMillis = Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS); - // TODO(255767350): demote all jobs to regular for user stops so they don't keep privileges JobStatus newJob = new JobStatus(failureToReschedule, elapsedNowMillis + delayMillis, JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops, failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis()); + 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 (job.isPeriodic()) { newJob.setOriginalLatestRunTimeElapsed( failureToReschedule.getOriginalLatestRunTimeElapsed()); @@ -2518,8 +2541,8 @@ public class JobSchedulerService extends com.android.server.SystemService * @param needsReschedule Whether the implementing class should reschedule this job. */ @Override - public void onJobCompletedLocked(JobStatus jobStatus, int debugStopReason, - boolean needsReschedule) { + public void onJobCompletedLocked(JobStatus jobStatus, @JobParameters.StopReason int stopReason, + int debugStopReason, boolean needsReschedule) { if (DEBUG) { Slog.d(TAG, "Completed " + jobStatus + ", reason=" + debugStopReason + ", reschedule=" + needsReschedule); @@ -2546,7 +2569,7 @@ public class JobSchedulerService extends com.android.server.SystemService // job so we can transfer any appropriate state over from the previous job when // we stop it. final JobStatus rescheduledJob = needsReschedule - ? getRescheduleJobForFailureLocked(jobStatus, debugStopReason) : null; + ? getRescheduleJobForFailureLocked(jobStatus, stopReason, debugStopReason) : null; if (rescheduledJob != null && !rescheduledJob.shouldTreatAsUserInitiatedJob() && (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index c2168d2e32787..911744f844be8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -1194,6 +1194,7 @@ public final class JobServiceContext implements ServiceConnection { applyStoppedReasonLocked(reason); completedJob = mRunningJob; final int internalStopReason = mParams.getInternalStopReasonCode(); + final int stopReason = mParams.getStopReason(); mPreviousJobHadSuccessfulFinish = (internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); if (!mPreviousJobHadSuccessfulFinish) { @@ -1214,7 +1215,7 @@ public final class JobServiceContext implements ServiceConnection { completedJob.hasContentTriggerConstraint(), completedJob.isRequestedExpeditedJob(), completedJob.startedAsExpeditedJob, - mParams.getStopReason(), + stopReason, completedJob.getJob().isPrefetch(), completedJob.getJob().getPriority(), completedJob.getEffectivePriority(), @@ -1267,7 +1268,8 @@ public final class JobServiceContext implements ServiceConnection { if (completedJob.isUserVisibleJob()) { mService.informObserversOfUserVisibleJobChange(this, completedJob, false); } - mCompletedListener.onJobCompletedLocked(completedJob, internalStopReason, reschedule); + mCompletedListener.onJobCompletedLocked(completedJob, stopReason, internalStopReason, + reschedule); mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType); } 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 9a69fdf2c27ba..f066b23fae846 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 @@ -373,6 +373,11 @@ public final class JobStatus { * @hide */ public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0; + /** + * Flag for {@link #mInternalFlags}: this job was stopped by the user for some reason + * 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; /** Minimum difference between start and end time to have flexible constraint */ @VisibleForTesting @@ -1380,8 +1385,8 @@ public final class JobStatus { * for any reason. */ public boolean shouldTreatAsUserInitiatedJob() { - // TODO(248386641): update implementation to handle loss of privilege - return getJob().isUserInitiated(); + return getJob().isUserInitiated() + && (getInternalFlags() & INTERNAL_FLAG_DEMOTED_BY_USER) == 0; } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index a8b8f917b0b18..7281fafc93f2e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -30,6 +30,7 @@ import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -370,11 +371,12 @@ public class JobSchedulerServiceTest { } /** - * Confirm that {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int)} + * Confirm that + * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)} * returns a job with the correct delay and deadline constraints. */ @Test - public void testGetRescheduleJobForFailure() { + public void testGetRescheduleJobForFailure_timingCalculations() { final long nowElapsed = sElapsedRealtimeClock.millis(); final long initialBackoffMs = MINUTE_IN_MILLIS; mService.mConstants.SYSTEM_STOP_TO_FAILURE_RATIO = 3; @@ -387,15 +389,18 @@ public class JobSchedulerServiceTest { // failure = 0, systemStop = 1 JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob, + JobParameters.STOP_REASON_DEVICE_STATE, JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL); assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime()); assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); // failure = 0, systemStop = 2 rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_DEVICE_STATE, JobParameters.INTERNAL_STOP_REASON_PREEMPT); // failure = 0, systemStop = 3 rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_CONSTRAINT_CHARGING, JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED); assertEquals(nowElapsed + initialBackoffMs, rescheduledJob.getEarliestRunTime()); assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); @@ -403,6 +408,7 @@ public class JobSchedulerServiceTest { // failure = 0, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO for (int i = 0; i < mService.mConstants.SYSTEM_STOP_TO_FAILURE_RATIO; ++i) { rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_SYSTEM_PROCESSING, JobParameters.INTERNAL_STOP_REASON_RTC_UPDATED); } assertEquals(nowElapsed + 2 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); @@ -410,17 +416,51 @@ public class JobSchedulerServiceTest { // failure = 1, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT); assertEquals(nowElapsed + 3 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); // failure = 2, systemStop = 2 * SYSTEM_STOP_TO_FAILURE_RATIO rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); assertEquals(nowElapsed + 4 * initialBackoffMs, rescheduledJob.getEarliestRunTime()); assertEquals(JobStatus.NO_LATEST_RUNTIME, rescheduledJob.getLatestRunTimeElapsed()); } + /** + * Confirm that + * {@link JobSchedulerService#getRescheduleJobForFailureLocked(JobStatus, int, int)} + * returns a job that is correctly marked as demoted by the user. + */ + @Test + public void testGetRescheduleJobForFailure_userDemotion() { + JobStatus originalJob = createJobStatus("testGetRescheduleJobForFailure", createJobInfo()); + assertEquals(0, originalJob.getInternalFlags() & JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + + // Reschedule for a non-user reason + JobStatus rescheduledJob = mService.getRescheduleJobForFailureLocked(originalJob, + JobParameters.STOP_REASON_DEVICE_STATE, + JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL); + assertEquals(0, + rescheduledJob.getInternalFlags() & JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + + // Reschedule for a user reason + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_USER, + JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP); + assertNotEquals(0, + rescheduledJob.getInternalFlags() & JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + + // Reschedule a previously demoted job for a non-user reason + rescheduledJob = mService.getRescheduleJobForFailureLocked(rescheduledJob, + JobParameters.STOP_REASON_CONSTRAINT_CHARGING, + JobParameters.INTERNAL_STOP_REASON_CONSTRAINTS_NOT_SATISFIED); + assertNotEquals(0, + rescheduledJob.getInternalFlags() & JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + } + /** * Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job * with the correct delay and deadline constraints if the periodic job is scheduled with the @@ -731,6 +771,7 @@ public class JobSchedulerServiceTest { JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_insideWindow_failedJob", createJobInfo().setPeriodic(HOUR_IN_MILLIS)); JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); JobStatus rescheduledJob = mService.getRescheduleJobForPeriodic(failedJob); @@ -739,6 +780,7 @@ public class JobSchedulerServiceTest { advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 5 minutes failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); advanceElapsedClock(5 * MINUTE_IN_MILLIS); // now + 10 minutes @@ -748,6 +790,7 @@ public class JobSchedulerServiceTest { advanceElapsedClock(35 * MINUTE_IN_MILLIS); // now + 45 minutes failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); advanceElapsedClock(10 * MINUTE_IN_MILLIS); // now + 55 minutes @@ -759,6 +802,7 @@ public class JobSchedulerServiceTest { advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 57 minutes failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); advanceElapsedClock(2 * MINUTE_IN_MILLIS); // now + 59 minutes @@ -856,6 +900,7 @@ public class JobSchedulerServiceTest { JobStatus job = createJobStatus("testGetRescheduleJobForPeriodic_outsideWindow_failedJob", createJobInfo().setPeriodic(HOUR_IN_MILLIS)); JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); long now = sElapsedRealtimeClock.millis(); long nextWindowStartTime = now + HOUR_IN_MILLIS; @@ -893,6 +938,7 @@ public class JobSchedulerServiceTest { "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob", createJobInfo().setPeriodic(HOUR_IN_MILLIS, 30 * MINUTE_IN_MILLIS)); JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); // First window starts 30 minutes from now. advanceElapsedClock(30 * MINUTE_IN_MILLIS); @@ -935,6 +981,7 @@ public class JobSchedulerServiceTest { "testGetRescheduleJobForPeriodic_outsideWindow_flex_failedJob_longPeriod", createJobInfo().setPeriodic(7 * DAY_IN_MILLIS, 9 * HOUR_IN_MILLIS)); JobStatus failedJob = mService.getRescheduleJobForFailureLocked(job, + JobParameters.STOP_REASON_UNDEFINED, JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH); // First window starts 6.625 days from now. advanceElapsedClock(6 * DAY_IN_MILLIS + 15 * HOUR_IN_MILLIS); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 6df54a693b5de..2c47fd9f50682 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -469,6 +469,34 @@ public class JobStatusTest { assertEquals(JobInfo.PRIORITY_LOW, job.getEffectivePriority()); } + @Test + public void testShouldTreatAsUserInitiated() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setUserInitiated(false) + .build(); + JobStatus job = createJobStatus(jobInfo); + + assertFalse(job.shouldTreatAsUserInitiatedJob()); + + jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setUserInitiated(true) + .build(); + job = createJobStatus(jobInfo); + + assertTrue(job.shouldTreatAsUserInitiatedJob()); + + JobStatus rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, + 0, 0, 0, 0); + assertTrue(rescheduledJob.shouldTreatAsUserInitiatedJob()); + + job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER); + assertFalse(job.shouldTreatAsUserInitiatedJob()); + + rescheduledJob = new JobStatus(job, NO_EARLIEST_RUNTIME, NO_LATEST_RUNTIME, + 0, 0, 0, 0); + assertFalse(rescheduledJob.shouldTreatAsUserInitiatedJob()); + } + /** * Test {@link JobStatus#wouldBeReadyWithConstraint} on explicit constraints that weren't * requested.