Demote jobs if the user stops the app.

If the user stops an app for any reason (Task Manager,
force stop, etc.), then demote any pending jobs (that the user wasn't
aware of) so that JS no longer treats them as user-initiated.

Bug: 255767350
Test: atest FrameworksMockingServicesTests:JobSchedulerServiceTest
Test: atest FrameworksMockingServicesTests:JobStatusTest
Change-Id: I6a6e971a1e86d9e5c65dac744e29b1d9418a5eb3
This commit is contained in:
Kweku Adams
2022-12-09 16:25:24 +00:00
parent d7c5239fc2
commit 9fc560f1e1
6 changed files with 127 additions and 16 deletions

View File

@@ -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);
}

View File

@@ -1692,10 +1692,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<JobStatus> 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);
}
}
}
@@ -2349,7 +2367,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();
@@ -2357,9 +2375,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++;
@@ -2390,11 +2410,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());
@@ -2515,8 +2538,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);
@@ -2543,7 +2566,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
&& (debugStopReason == JobParameters.INTERNAL_STOP_REASON_TIMEOUT
|| debugStopReason == JobParameters.INTERNAL_STOP_REASON_PREEMPT)) {

View File

@@ -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);
}

View File

@@ -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;
}
/**

View File

@@ -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);

View File

@@ -376,6 +376,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.