Merge "Demote jobs if the user stops the app."
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user