diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java index 5d67c96213a5d..50a7a194e8a30 100644 --- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java +++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java @@ -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) { } } diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl index a1f19542c4abf..1ea0c5bea5791 100644 --- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl +++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl @@ -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); } diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index e0fffb4c152ea..d4110a317cfe4 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -1897,7 +1897,11 @@ public class JobInfo implements Parcelable { *

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

* If the app doesn't hold the {@link android.Manifest.permission#RUN_LONG_JOBS} permission diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java index 4aec484aad093..3ba5c31873bd1 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -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); } diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java index 1a205d0008358..2ab432488df53 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobService.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java @@ -156,6 +156,12 @@ public abstract class JobService extends Service { * a future idle maintenance window. *

* + *

+ * 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.

* + *

+ * 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}. + * *

Note: 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. diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 62d9735805019..a474cc836f2c5 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -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); } } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index e8fcdd2979ded..fce75a2a0897e 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -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 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); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 514d98445c721..cc9e517b2dfc0 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -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); } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index ce33a8e08b4af..1971a11ca98ae 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -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) { diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index b9373be76b9ae..1084c71397e4d 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -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. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 464b6e70b7d54..048d40c2a33d1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -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() { diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index ed78e720df73b..c7bacbcfc0550 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -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)); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 2c47fd9f50682..e6bc72f94f05b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -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)