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)