Update response to Task Manager stops.

1. Don't inform the job it's about to be stopped.
2. Don't reschedule user-visible jobs if they were stopped by the user
   via Task Manager.
3. Kill the app process immediately instead of waiting for the onStopJob
   flow/timeout.

Bug: 261999509
Test: atest FrameworksMockingServicesTests:JobSchedulerServiceTest
Test: atest FrameworksMockingServicesTests:JobStatusTest
Test: atest SystemUITests:FgsManagerControllerTest
Test: Manually stop test app in Task Manager and check JobScheduler logs
Change-Id: I29cecfc4b0988685e78387943154837cd4b72cea
This commit is contained in:
Kweku Adams
2023-01-23 20:51:34 +00:00
parent 022f67e267
commit 677c6ae13a
13 changed files with 248 additions and 44 deletions

View File

@@ -230,9 +230,10 @@ public class JobSchedulerImpl extends JobScheduler {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
@Nullable String debugReason) {
try {
mBinder.stopUserVisibleJobsForUser(packageName, userId);
mBinder.notePendingUserRequestedAppStop(packageName, userId, debugReason);
} catch (RemoteException e) {
}
}

View File

@@ -48,5 +48,5 @@ interface IJobScheduler {
@EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
void unregisterUserVisibleJobObserver(in IUserVisibleJobObserver observer);
@EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
void stopUserVisibleJobsForUser(String packageName, int userId);
void notePendingUserRequestedAppStop(String packageName, int userId, String debugReason);
}

View File

@@ -1897,7 +1897,11 @@ public class JobInfo implements Parcelable {
* <p>
* All user-initiated jobs must have an associated notification, set via
* {@link JobService#setNotification(JobParameters, int, Notification, int)}, and will be
* shown in the Task Manager when running.
* shown in the Task Manager when running. These jobs cannot be rescheduled by the app
* if the user stops the job via system provided affordance (such as the Task Manager).
* Thus, it is best practice and recommended to provide action buttons in the
* associated notification to allow the user to stop the job gracefully
* and allow for rescheduling.
*
* <p>
* If the app doesn't hold the {@link android.Manifest.permission#RUN_LONG_JOBS} permission

View File

@@ -515,5 +515,6 @@ public abstract class JobScheduler {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@SuppressWarnings("HiddenAbstractMethod")
public abstract void stopUserVisibleJobsForUser(@NonNull String packageName, int userId);
public abstract void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
@Nullable String debugReason);
}

View File

@@ -156,6 +156,12 @@ public abstract class JobService extends Service {
* a future idle maintenance window.
* </p>
*
* <p class="note">
* Any {@link JobInfo.Builder#setUserInitiated(boolean) user-initiated job}
* cannot be rescheduled when the user has asked to stop the app
* via a system provided affordance (such as the Task Manager).
* In such situations, the value of {@code wantsReschedule} is always treated as {@code false}.
*
* @param params The parameters identifying this job, as supplied to
* the job in the {@link #onStartJob(JobParameters)} callback.
* @param wantsReschedule {@code true} if this job should be rescheduled according
@@ -220,6 +226,12 @@ public abstract class JobService extends Service {
* Once this method returns (or times out), the system releases the wakelock that it is holding
* on behalf of the job.</p>
*
* <p class="note">
* Any {@link JobInfo.Builder#setUserInitiated(boolean) user-initiated job}
* cannot be rescheduled when stopped by the user via a system provided affordance (such as
* the Task Manager). In such situations, the returned value from this method call is always
* treated as {@code false}.
*
* <p class="caution"><strong>Note:</strong> When a job is stopped and rescheduled via this
* method call, the deadline constraint is excluded from the rescheduled job's constraint set.
* The rescheduled job will run again once all remaining constraints are satisfied.

View File

@@ -1282,17 +1282,20 @@ class JobConcurrencyManager {
}
@GuardedBy("mLock")
void stopUserVisibleJobsLocked(int userId, @NonNull String packageName,
@JobParameters.StopReason int reason, int internalReasonCode) {
void markJobsForUserStopLocked(int userId, @NonNull String packageName,
@Nullable String debugReason) {
for (int i = mActiveServices.size() - 1; i >= 0; --i) {
final JobServiceContext jsc = mActiveServices.get(i);
final JobStatus jobStatus = jsc.getRunningJobLocked();
if (jobStatus != null && userId == jobStatus.getSourceUserId()
&& jobStatus.getSourcePackageName().equals(packageName)
&& jobStatus.isUserVisibleJob()) {
jsc.cancelExecutingJobLocked(reason, internalReasonCode,
JobParameters.getInternalReasonCodeDescription(internalReasonCode));
// Normally, we handle jobs primarily using the source package and userId,
// however, user-visible jobs are shown as coming from the calling app, so we
// need to operate on the jobs from that perspective here.
if (jobStatus != null && userId == jobStatus.getUserId()
&& jobStatus.getServiceComponent().getPackageName().equals(packageName)) {
jsc.markForProcessDeathLocked(JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP,
debugReason);
}
}
}

View File

@@ -1701,16 +1701,15 @@ public class JobSchedulerService extends com.android.server.SystemService
}
@VisibleForTesting
void stopUserVisibleJobsInternal(@NonNull String packageName, int userId) {
void notePendingUserRequestedAppStopInternal(@NonNull String packageName, int userId,
@Nullable String debugReason) {
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);
mConcurrencyManager.markJobsForUserStopLocked(userId, packageName, debugReason);
final ArraySet<JobStatus> jobs = mJobs.getJobsByUid(packageUid);
for (int i = jobs.size() - 1; i >= 0; i--) {
final JobStatus job = jobs.valueAt(i);
@@ -2387,12 +2386,25 @@ public class JobSchedulerService extends com.android.server.SystemService
*
* @param failureToReschedule Provided job status that we will reschedule.
* @return A newly instantiated JobStatus with the same constraints as the last job except
* with adjusted timing constraints.
* with adjusted timing constraints, or {@code null} if the job shouldn't be rescheduled for
* some policy reason.
* @see #maybeQueueReadyJobsForExecutionLocked
*/
@Nullable
@VisibleForTesting
JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule,
@JobParameters.StopReason int stopReason, int internalStopReason) {
if (internalStopReason == JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP
&& failureToReschedule.isUserVisibleJob()) {
// If a user stops an app via Task Manager and the job was user-visible, then assume
// the user wanted to stop that task and not let it run in the future. It's in the
// app's best interests to provide action buttons in their notification to avoid this
// scenario.
Slog.i(TAG,
"Dropping " + failureToReschedule.toShortString() + " because of user stop");
return null;
}
final long elapsedNowMillis = sElapsedRealtimeClock.millis();
final JobInfo job = failureToReschedule.getJob();
@@ -4225,12 +4237,13 @@ public class JobSchedulerService extends com.android.server.SystemService
@Override
@EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
super.stopUserVisibleJobsForUser_enforcePermission();
public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
@Nullable String debugReason) {
super.notePendingUserRequestedAppStop_enforcePermission();
if (packageName == null) {
throw new NullPointerException("packageName");
}
JobSchedulerService.this.stopUserVisibleJobsInternal(packageName, userId);
notePendingUserRequestedAppStopInternal(packageName, userId, debugReason);
}
}

View File

@@ -190,6 +190,14 @@ public final class JobServiceContext implements ServiceConnection {
private Network mPendingNetworkChange;
/**
* The reason this job is marked for death. If it's not marked for death,
* then the value should be {@link JobParameters#STOP_REASON_UNDEFINED}.
*/
private int mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
private int mDeathMarkInternalStopReason;
private String mDeathMarkDebugReason;
// Debugging: reason this job was last stopped.
public String mStoppedReason;
@@ -452,6 +460,7 @@ public final class JobServiceContext implements ServiceConnection {
mStoppedReason = null;
mStoppedTime = 0;
job.startedAsExpeditedJob = job.shouldTreatAsExpeditedJob();
job.startedAsUserInitiatedJob = job.shouldTreatAsUserInitiatedJob();
return true;
}
}
@@ -502,6 +511,34 @@ public final class JobServiceContext implements ServiceConnection {
doCancelLocked(reason, internalStopReason, debugReason);
}
/**
* Called when an app's process is about to be killed and we want to update the job's stop
* reasons without telling the job it's going to be stopped.
*/
@GuardedBy("mLock")
void markForProcessDeathLocked(@JobParameters.StopReason int reason,
int internalStopReason, @NonNull String debugReason) {
if (mVerb == VERB_FINISHED) {
if (DEBUG) {
Slog.d(TAG, "Too late to mark for death (verb=" + mVerb + "), ignoring.");
}
return;
}
if (DEBUG) {
Slog.d(TAG,
"Marking " + mRunningJob.toShortString() + " for death because "
+ reason + ":" + debugReason);
}
mDeathMarkStopReason = reason;
mDeathMarkInternalStopReason = internalStopReason;
mDeathMarkDebugReason = debugReason;
if (mParams.getStopReason() == JobParameters.STOP_REASON_UNDEFINED) {
// Only set the stop reason if we're not already trying to stop the job for some
// other reason in case that other stop is successful before the process dies.
mParams.setStopReason(reason, internalStopReason, debugReason);
}
}
int getPreferredUid() {
return mPreferredUid;
}
@@ -754,6 +791,12 @@ public final class JobServiceContext implements ServiceConnection {
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
if (mDeathMarkStopReason != JobParameters.STOP_REASON_UNDEFINED) {
// Service "unexpectedly" disconnected, but we knew the process was going to die.
// Use that as the stop reason for logging/debugging purposes.
mParams.setStopReason(
mDeathMarkStopReason, mDeathMarkInternalStopReason, mDeathMarkDebugReason);
}
closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected");
}
}
@@ -1182,29 +1225,51 @@ public final class JobServiceContext implements ServiceConnection {
* we want to clean up internally.
*/
@GuardedBy("mLock")
private void closeAndCleanupJobLocked(boolean reschedule, @Nullable String reason) {
private void closeAndCleanupJobLocked(boolean reschedule, @Nullable String loggingDebugReason) {
final JobStatus completedJob;
if (mVerb == VERB_FINISHED) {
return;
}
if (DEBUG) {
Slog.d(TAG, "Cleaning up " + mRunningJob.toShortString()
+ " reschedule=" + reschedule + " reason=" + reason);
+ " reschedule=" + reschedule + " reason=" + loggingDebugReason);
}
applyStoppedReasonLocked(reason);
applyStoppedReasonLocked(loggingDebugReason);
completedJob = mRunningJob;
final int internalStopReason = mParams.getInternalStopReasonCode();
final int stopReason = mParams.getStopReason();
// Use the JobParameters stop reasons for logging and metric purposes,
// but if the job was marked for death, use that reason for rescheduling purposes.
// The discrepancy could happen if a job ends up stopping for some reason
// in the time between the job being marked and the process actually dying.
// Since the job stopped for another reason, we want to log the actual stop reason
// for the sake of accurate metrics and debugging,
// but we should use the death mark reasons when determining reschedule policy.
final int loggingStopReason = mParams.getStopReason();
final int loggingInternalStopReason = mParams.getInternalStopReasonCode();
final int reschedulingStopReason, reschedulingInternalStopReason;
if (mDeathMarkStopReason != JobParameters.STOP_REASON_UNDEFINED) {
if (DEBUG) {
Slog.d(TAG, "Job marked for death because of "
+ JobParameters.getInternalReasonCodeDescription(
mDeathMarkInternalStopReason)
+ ": " + mDeathMarkDebugReason);
}
reschedulingStopReason = mDeathMarkStopReason;
reschedulingInternalStopReason = mDeathMarkInternalStopReason;
} else {
reschedulingStopReason = loggingStopReason;
reschedulingInternalStopReason = loggingInternalStopReason;
}
mPreviousJobHadSuccessfulFinish =
(internalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
(loggingInternalStopReason == JobParameters.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH);
if (!mPreviousJobHadSuccessfulFinish) {
mLastUnsuccessfulFinishElapsed = sElapsedRealtimeClock.millis();
}
mJobPackageTracker.noteInactive(completedJob, internalStopReason, reason);
mJobPackageTracker.noteInactive(completedJob,
loggingInternalStopReason, loggingDebugReason);
FrameworkStatsLog.write_non_chained(FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED,
completedJob.getSourceUid(), null, completedJob.getBatteryName(),
FrameworkStatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__FINISHED,
internalStopReason, completedJob.getStandbyBucket(), completedJob.getJobId(),
loggingInternalStopReason, completedJob.getStandbyBucket(), completedJob.getJobId(),
completedJob.hasChargingConstraint(),
completedJob.hasBatteryNotLowConstraint(),
completedJob.hasStorageNotLowConstraint(),
@@ -1215,7 +1280,7 @@ public final class JobServiceContext implements ServiceConnection {
completedJob.hasContentTriggerConstraint(),
completedJob.isRequestedExpeditedJob(),
completedJob.startedAsExpeditedJob,
stopReason,
loggingStopReason,
completedJob.getJob().isPrefetch(),
completedJob.getJob().getPriority(),
completedJob.getEffectivePriority(),
@@ -1235,11 +1300,11 @@ public final class JobServiceContext implements ServiceConnection {
}
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
internalStopReason);
loggingInternalStopReason);
} catch (RemoteException e) {
// Whatever.
}
if (mParams.getStopReason() == JobParameters.STOP_REASON_TIMEOUT) {
if (loggingStopReason == JobParameters.STOP_REASON_TIMEOUT) {
mEconomyManagerInternal.noteInstantaneousEvent(
mRunningJob.getSourceUserId(), mRunningJob.getSourcePackageName(),
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
@@ -1260,6 +1325,9 @@ public final class JobServiceContext implements ServiceConnection {
mCancelled = false;
service = null;
mAvailable = true;
mDeathMarkStopReason = JobParameters.STOP_REASON_UNDEFINED;
mDeathMarkInternalStopReason = 0;
mDeathMarkDebugReason = null;
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingInternalStopReason = 0;
mPendingDebugStopReason = null;
@@ -1268,8 +1336,8 @@ public final class JobServiceContext implements ServiceConnection {
if (completedJob.isUserVisibleJob()) {
mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
}
mCompletedListener.onJobCompletedLocked(completedJob, stopReason, internalStopReason,
reschedule);
mCompletedListener.onJobCompletedLocked(completedJob,
reschedulingStopReason, reschedulingInternalStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
}

View File

@@ -402,6 +402,11 @@ public final class JobStatus {
* running. This isn't copied over when a job is rescheduled.
*/
public boolean startedAsExpeditedJob = false;
/**
* Whether or not this particular JobStatus instance was treated as a user-initiated job
* when it started running. This isn't copied over when a job is rescheduled.
*/
public boolean startedAsUserInitiatedJob = false;
public boolean startedWithImmediacyPrivilege = false;
@@ -1407,7 +1412,7 @@ public final class JobStatus {
* @return true if this is a job whose execution should be made visible to the user.
*/
public boolean isUserVisibleJob() {
return shouldTreatAsUserInitiatedJob();
return shouldTreatAsUserInitiatedJob() || startedAsUserInitiatedJob;
}
/**
@@ -2568,6 +2573,13 @@ public final class JobStatus {
pw.print(startedAsExpeditedJob);
pw.println(")");
}
if ((getFlags() & JobInfo.FLAG_USER_INITIATED) != 0) {
pw.print("userInitiatedApproved: ");
pw.print(shouldTreatAsUserInitiatedJob());
pw.print(" (started as UIJ: ");
pw.print(startedAsUserInitiatedJob);
pw.println(")");
}
pw.decreaseIndent();
if (changedAuthorities != null) {

View File

@@ -561,6 +561,13 @@ public final class SystemUiDeviceConfigFlags {
public static final String TASK_MANAGER_SHOW_USER_VISIBLE_JOBS =
"task_manager_show_user_visible_jobs";
/**
* (boolean) Whether the task manager should tell JobScheduler it's about to ask for an
* app stop.
*/
public static final String TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP =
"task_manager_inform_job_scheduler_of_pending_app_stop";
/**
* (boolean) Whether to show notification volume control slider separate from ring.
*/

View File

@@ -48,6 +48,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
@@ -158,6 +159,7 @@ class FgsManagerControllerImpl @Inject constructor(
private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = true
private const val DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP = true
}
override var newChangesSinceDialogWasDismissed = false
@@ -173,6 +175,9 @@ class FgsManagerControllerImpl @Inject constructor(
private var showUserVisibleJobs = DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
private var informJobSchedulerOfPendingAppStop =
DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP
override val includesUserVisibleJobs: Boolean
get() = showUserVisibleJobs
@@ -233,6 +238,11 @@ class FgsManagerControllerImpl @Inject constructor(
NAMESPACE_SYSTEMUI,
TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS)
informJobSchedulerOfPendingAppStop = deviceConfigProxy.getBoolean(
NAMESPACE_SYSTEMUI,
TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP,
DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP)
try {
activityManager.registerForegroundServiceObserver(foregroundServiceObserver)
// Clumping FGS and user-visible jobs here and showing a single entry and button
@@ -262,10 +272,13 @@ class FgsManagerControllerImpl @Inject constructor(
showStopBtnForUserAllowlistedApps)
var wasShowingUserVisibleJobs = showUserVisibleJobs
showUserVisibleJobs = it.getBoolean(
TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, showUserVisibleJobs)
TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, showUserVisibleJobs)
if (showUserVisibleJobs != wasShowingUserVisibleJobs) {
onShowUserVisibleJobsFlagChanged()
}
informJobSchedulerOfPendingAppStop = it.getBoolean(
TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
informJobSchedulerOfPendingAppStop)
}
_isAvailable.value = deviceConfigProxy.getBoolean(
@@ -475,14 +488,11 @@ class FgsManagerControllerImpl @Inject constructor(
private fun stopPackage(userId: Int, packageName: String, timeStarted: Long) {
logEvent(stopped = true, packageName, userId, timeStarted)
val userPackageKey = UserPackage(userId, packageName)
if (showUserVisibleJobs &&
runningTaskIdentifiers[userPackageKey]?.hasRunningJobs() == true) {
if (showUserVisibleJobs || informJobSchedulerOfPendingAppStop) {
// TODO(255768978): allow fine-grained job control
jobScheduler.stopUserVisibleJobsForUser(packageName, userId)
}
if (runningTaskIdentifiers[userPackageKey]?.hasFgs() == true) {
activityManager.stopAppForUser(packageName, userId)
jobScheduler.notePendingUserRequestedAppStop(packageName, userId, "task manager")
}
activityManager.stopAppForUser(packageName, userId)
}
private fun onShowUserVisibleJobsFlagChanged() {

View File

@@ -32,6 +32,8 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
@@ -469,6 +471,50 @@ public class JobSchedulerServiceTest {
rescheduledJob.getInternalFlags() & JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
}
/**
* Confirm that
* returns {@code null} when for user-visible jobs stopped by the user.
*/
@Test
public void testGetRescheduleJobForFailure_userStopped() {
JobStatus uiJob = createJobStatus("testGetRescheduleJobForFailure",
createJobInfo().setUserInitiated(true));
JobStatus uvJob = createJobStatus("testGetRescheduleJobForFailure", createJobInfo());
spyOn(uvJob);
doReturn(true).when(uvJob).isUserVisibleJob();
JobStatus regJob = createJobStatus("testGetRescheduleJobForFailure", createJobInfo());
// Reschedule for a non-user reason
JobStatus rescheduledUiJob = mService.getRescheduleJobForFailureLocked(uiJob,
JobParameters.STOP_REASON_DEVICE_STATE,
JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
JobStatus rescheduledUvJob = mService.getRescheduleJobForFailureLocked(uvJob,
JobParameters.STOP_REASON_DEVICE_STATE,
JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
JobStatus rescheduledRegJob = mService.getRescheduleJobForFailureLocked(regJob,
JobParameters.STOP_REASON_DEVICE_STATE,
JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL);
assertNotNull(rescheduledUiJob);
assertNotNull(rescheduledUvJob);
assertNotNull(rescheduledRegJob);
// Reschedule for a user reason. The user-visible jobs shouldn't be rescheduled.
spyOn(rescheduledUvJob);
doReturn(true).when(rescheduledUvJob).isUserVisibleJob();
rescheduledUiJob = mService.getRescheduleJobForFailureLocked(rescheduledUiJob,
JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
rescheduledUvJob = mService.getRescheduleJobForFailureLocked(rescheduledUvJob,
JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
rescheduledRegJob = mService.getRescheduleJobForFailureLocked(rescheduledRegJob,
JobParameters.STOP_REASON_USER,
JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
assertNull(rescheduledUiJob);
assertNull(rescheduledUvJob);
assertNotNull(rescheduledRegJob);
}
/**
* Confirm that {@link JobSchedulerService#getRescheduleJobForPeriodic(JobStatus)} returns a job
* with the correct delay and deadline constraints if the periodic job is scheduled with the
@@ -1274,14 +1320,14 @@ public class JobSchedulerServiceTest {
mService.getJobStore().add(job2a);
mService.getJobStore().add(job2b);
mService.stopUserVisibleJobsInternal("pkg1", 1);
mService.notePendingUserRequestedAppStopInternal("pkg1", 1, "test");
assertEquals(4, mService.getPendingJobQueue().size());
assertTrue(mService.getPendingJobQueue().contains(job1a));
assertTrue(mService.getPendingJobQueue().contains(job1b));
assertTrue(mService.getPendingJobQueue().contains(job2a));
assertTrue(mService.getPendingJobQueue().contains(job2b));
mService.stopUserVisibleJobsInternal("pkg1", 0);
mService.notePendingUserRequestedAppStopInternal("pkg1", 0, "test");
assertEquals(2, mService.getPendingJobQueue().size());
assertFalse(mService.getPendingJobQueue().contains(job1a));
assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job1a));
@@ -1290,7 +1336,7 @@ public class JobSchedulerServiceTest {
assertTrue(mService.getPendingJobQueue().contains(job2a));
assertTrue(mService.getPendingJobQueue().contains(job2b));
mService.stopUserVisibleJobsInternal("pkg2", 0);
mService.notePendingUserRequestedAppStopInternal("pkg2", 0, "test");
assertEquals(0, mService.getPendingJobQueue().size());
assertFalse(mService.getPendingJobQueue().contains(job1a));
assertFalse(mService.getPendingJobQueue().contains(job1b));

View File

@@ -224,6 +224,33 @@ public class JobStatusTest {
assertTrue(job.canRunInDoze());
}
@Test
public void testIsUserVisibleJob() {
JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
.setUserInitiated(false)
.build();
JobStatus job = createJobStatus(jobInfo);
assertFalse(job.isUserVisibleJob());
// User-initiated jobs are always user-visible unless they've been demoted.
jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
.setUserInitiated(true)
.build();
job = createJobStatus(jobInfo);
assertTrue(job.isUserVisibleJob());
job.addInternalFlags(JobStatus.INTERNAL_FLAG_DEMOTED_BY_USER);
assertFalse(job.isUserVisibleJob());
job.startedAsUserInitiatedJob = true;
assertTrue(job.isUserVisibleJob());
job.startedAsUserInitiatedJob = false;
assertFalse(job.isUserVisibleJob());
}
@Test
public void testMediaBackupExemption_lateConstraint() {
final JobInfo triggerContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)