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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user