Merge "Implement JS <-> TaskManager user-visible job link."
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 2d3201a..4242cf8 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -158,7 +158,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
- // TODO(255767350): implement
+ try {
+ mBinder.registerUserVisibleJobObserver(observer);
+ } catch (RemoteException e) {
+ }
}
@RequiresPermission(allOf = {
@@ -166,7 +169,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
- // TODO(255767350): implement
+ try {
+ mBinder.unregisterUserVisibleJobObserver(observer);
+ } catch (RemoteException e) {
+ }
}
@RequiresPermission(allOf = {
@@ -174,6 +180,9 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
@Override
public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
- // TODO(255767350): implement
+ try {
+ mBinder.stopUserVisibleJobsForUser(packageName, userId);
+ } 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 bf29dc9..c87a2af 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -16,6 +16,7 @@
package android.app.job;
+import android.app.job.IUserVisibleJobObserver;
import android.app.job.JobInfo;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
@@ -38,4 +39,10 @@
boolean hasRunLongJobsPermission(String packageName, int userId);
List<JobInfo> getStartedJobs();
ParceledListSlice getAllJobSnapshots();
+ @EnforcePermission(allOf={"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ void registerUserVisibleJobObserver(in IUserVisibleJobObserver observer);
+ @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);
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index ed72530..0205430 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -98,6 +98,12 @@
*/
public static final int INTERNAL_STOP_REASON_SUCCESSFUL_FINISH =
JobProtoEnums.INTERNAL_STOP_REASON_SUCCESSFUL_FINISH; // 10.
+ /**
+ * The user stopped the job via some UI (eg. Task Manager).
+ * @hide
+ */
+ public static final int INTERNAL_STOP_REASON_USER_UI_STOP =
+ JobProtoEnums.INTERNAL_STOP_REASON_USER_UI_STOP; // 11.
/**
* All the stop reason codes. This should be regarded as an immutable array at runtime.
@@ -121,6 +127,7 @@
INTERNAL_STOP_REASON_DATA_CLEARED,
INTERNAL_STOP_REASON_RTC_UPDATED,
INTERNAL_STOP_REASON_SUCCESSFUL_FINISH,
+ INTERNAL_STOP_REASON_USER_UI_STOP,
};
/**
@@ -141,6 +148,7 @@
case INTERNAL_STOP_REASON_DATA_CLEARED: return "data_cleared";
case INTERNAL_STOP_REASON_RTC_UPDATED: return "rtc_updated";
case INTERNAL_STOP_REASON_SUCCESSFUL_FINISH: return "successful_finish";
+ case INTERNAL_STOP_REASON_USER_UI_STOP: return "user_ui_stop";
default: return "unknown:" + reasonCode;
}
}
@@ -230,7 +238,7 @@
public static final int STOP_REASON_APP_STANDBY = 12;
/**
* The user stopped the job. This can happen either through force-stop, adb shell commands,
- * or uninstalling.
+ * uninstalling, or some other UI.
*/
public static final int STOP_REASON_USER = 13;
/** The system is doing some processing that requires stopping this job. */
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 47f6890..16201b2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1184,6 +1184,22 @@
}
@GuardedBy("mLock")
+ void stopUserVisibleJobsLocked(int userId, @NonNull String packageName,
+ @JobParameters.StopReason int reason, int internalReasonCode) {
+ 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));
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
void stopNonReadyActiveJobsLocked() {
for (int i = 0; i < mActiveServices.size(); i++) {
JobServiceContext serviceContext = mActiveServices.get(i);
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 9fb2af7..ad6eff0 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -16,11 +16,14 @@
package com.android.server.job;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.MANAGE_ACTIVITY_TASKS;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -31,6 +34,7 @@
import android.app.IUidObserver;
import android.app.compat.CompatChanges;
import android.app.job.IJobScheduler;
+import android.app.job.IUserVisibleJobObserver;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobProtoEnums;
@@ -38,6 +42,7 @@
import android.app.job.JobService;
import android.app.job.JobSnapshot;
import android.app.job.JobWorkItem;
+import android.app.job.UserVisibleJobSummary;
import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.compat.annotation.ChangeId;
@@ -68,6 +73,7 @@
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -248,6 +254,8 @@
static final int MSG_UID_IDLE = 7;
static final int MSG_CHECK_CHANGED_JOB_LIST = 8;
static final int MSG_CHECK_MEDIA_EXEMPTION = 9;
+ static final int MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS = 10;
+ static final int MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE = 11;
/** List of controllers that will notify this service of updates to jobs. */
final List<StateController> mControllers;
@@ -279,6 +287,9 @@
@GuardedBy("mLock")
private final SparseArray<String> mCloudMediaProviderPackages = new SparseArray<>();
+ private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers =
+ new RemoteCallbackList<>();
+
private final CountQuotaTracker mQuotaTracker;
private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
private static final String QUOTA_TRACKER_SCHEDULE_LOGGED =
@@ -1501,6 +1512,14 @@
}
}
+ private void stopUserVisibleJobsInternal(@NonNull String packageName, int userId) {
+ synchronized (mLock) {
+ mConcurrencyManager.stopUserVisibleJobsLocked(userId, packageName,
+ JobParameters.STOP_REASON_USER,
+ JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
+ }
+ }
+
private final Consumer<JobStatus> mCancelJobDueToUserRemovalConsumer = (toRemove) -> {
// There's no guarantee that the process has been stopped by the time we get
// here, but since this is a user-initiated action, it should be fine to just
@@ -2159,6 +2178,7 @@
}
delayMillis =
Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
+ // TODO(255767350): demote all jobs to regular for user stops so they don't keep privileges
JobStatus newJob = new JobStatus(failureToReschedule,
elapsedNowMillis + delayMillis,
JobStatus.NO_LATEST_RUNTIME, numFailures, numSystemStops,
@@ -2509,6 +2529,52 @@
args.recycle();
break;
}
+
+ case MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS: {
+ final IUserVisibleJobObserver observer =
+ (IUserVisibleJobObserver) message.obj;
+ synchronized (mLock) {
+ for (int i = mConcurrencyManager.mActiveServices.size() - 1; i >= 0;
+ --i) {
+ JobServiceContext context =
+ mConcurrencyManager.mActiveServices.get(i);
+ final JobStatus jobStatus = context.getRunningJobLocked();
+ if (jobStatus != null && jobStatus.isUserVisibleJob()) {
+ try {
+ observer.onUserVisibleJobStateChanged(
+ jobStatus.getUserVisibleJobSummary(),
+ /* isRunning */ true);
+ } catch (RemoteException e) {
+ // Will be unregistered automatically by
+ // RemoteCallbackList's dead-object tracking,
+ // so don't need to remove it here.
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+
+ case MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE: {
+ final SomeArgs args = (SomeArgs) message.obj;
+ final JobServiceContext context = (JobServiceContext) args.arg1;
+ final JobStatus jobStatus = (JobStatus) args.arg2;
+ final UserVisibleJobSummary summary = jobStatus.getUserVisibleJobSummary();
+ final boolean isRunning = args.argi1 == 1;
+ for (int i = mUserVisibleJobObservers.beginBroadcast() - 1; i >= 0; --i) {
+ try {
+ mUserVisibleJobObservers.getBroadcastItem(i)
+ .onUserVisibleJobStateChanged(summary, isRunning);
+ } catch (RemoteException e) {
+ // Will be unregistered automatically by RemoteCallbackList's
+ // dead-object tracking, so nothing we need to do here.
+ }
+ }
+ mUserVisibleJobObservers.finishBroadcast();
+ args.recycle();
+ break;
+ }
}
maybeRunPendingJobsLocked();
}
@@ -3022,6 +3088,16 @@
return adjustJobBias(bias, job);
}
+ void informObserversOfUserVisibleJobChange(JobServiceContext context, JobStatus jobStatus,
+ boolean isRunning) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = context;
+ args.arg2 = jobStatus;
+ args.argi1 = isRunning ? 1 : 0;
+ mHandler.obtainMessage(MSG_INFORM_OBSERVERS_OF_USER_VISIBLE_JOB_CHANGE, args)
+ .sendToTarget();
+ }
+
private final class BatteryStateTracker extends BroadcastReceiver {
/**
* Track whether we're "charging", where charging means that we're ready to commit to
@@ -3744,6 +3820,35 @@
return new ParceledListSlice<>(snapshots);
}
}
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ if (observer == null) {
+ throw new NullPointerException("observer");
+ }
+ mUserVisibleJobObservers.register(observer);
+ mHandler.obtainMessage(MSG_INFORM_OBSERVER_OF_ALL_USER_VISIBLE_JOBS, observer)
+ .sendToTarget();
+ }
+
+ @Override
+ @EnforcePermission(allOf = {MANAGE_ACTIVITY_TASKS, INTERACT_ACROSS_USERS_FULL})
+ public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ if (observer == null) {
+ throw new NullPointerException("observer");
+ }
+ mUserVisibleJobObservers.unregister(observer);
+ }
+
+ @Override
+ @EnforcePermission(allOf = {"MANAGE_ACTIVITY_TASKS", "INTERACT_ACROSS_USERS_FULL"})
+ public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) {
+ if (packageName == null) {
+ throw new NullPointerException("packageName");
+ }
+ JobSchedulerService.this.stopUserVisibleJobsInternal(packageName, userId);
+ }
}
// Shell command infrastructure: run the given job immediately
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 b20eedc..fead68e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -944,6 +944,9 @@
return;
}
scheduleOpTimeOutLocked();
+ if (mRunningJob.isUserVisibleJob()) {
+ mService.informObserversOfUserVisibleJobChange(this, mRunningJob, true);
+ }
break;
default:
Slog.e(TAG, "Handling started job but job wasn't starting! Was "
@@ -1202,6 +1205,9 @@
mPendingDebugStopReason = null;
mNotification = null;
removeOpTimeOutLocked();
+ if (completedJob.isUserVisibleJob()) {
+ mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
+ }
mCompletedListener.onJobCompletedLocked(completedJob, internalStopReason, 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 af8e727..83b6a8e 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
@@ -29,11 +29,13 @@
import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
import android.app.AppGlobals;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobWorkItem;
+import android.app.job.UserVisibleJobSummary;
import android.content.ClipData;
import android.content.ComponentName;
import android.net.Network;
@@ -454,6 +456,12 @@
*/
private boolean mExpeditedTareApproved;
+ /**
+ * Summary describing this job. Lazily created in {@link #getUserVisibleJobSummary()}
+ * since not every job will need it.
+ */
+ private UserVisibleJobSummary mUserVisibleJobSummary;
+
/////// Booleans that track if a job is ready to run. They should be updated whenever dependent
/////// states change.
@@ -1337,6 +1345,27 @@
}
/**
+ * Return a summary that uniquely identifies the underlying job.
+ */
+ @NonNull
+ public UserVisibleJobSummary getUserVisibleJobSummary() {
+ if (mUserVisibleJobSummary == null) {
+ mUserVisibleJobSummary = new UserVisibleJobSummary(
+ callingUid, getSourceUserId(), getSourcePackageName(), getJobId());
+ }
+ return mUserVisibleJobSummary;
+ }
+
+ /**
+ * @return true if this is a job whose execution should be made visible to the user.
+ */
+ public boolean isUserVisibleJob() {
+ // TODO(255767350): limit to user-initiated jobs
+ // Placeholder implementation until we have the code in
+ return shouldTreatAsExpeditedJob();
+ }
+
+ /**
* @return true if the job is exempted from Doze restrictions and therefore allowed to run
* in Doze.
*/