Merge "Demote jobs if the user stops the app."

This commit is contained in:
Kweku Adams
2023-01-26 17:05:21 +00:00
committed by Android (Google) Code Review
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

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

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

@@ -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.