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