Merge "Add API to allow apps to tie a notification to a job."

This commit is contained in:
Kweku Adams
2022-12-06 15:40:37 +00:00
committed by Android (Google) Code Review
4 changed files with 192 additions and 1 deletions

View File

@@ -16,6 +16,7 @@
package android.app.job;
import android.app.Notification;
import android.app.job.JobWorkItem;
/**
@@ -104,4 +105,17 @@ interface IJobCallback {
*/
void updateTransferredNetworkBytes(int jobId, in JobWorkItem item,
long transferredDownloadBytes, long transferredUploadBytes);
/**
* Provide JobScheduler with a notification to post and tie to this job's
* lifecycle.
* This is required for all user-initiated job and optional for other jobs.
*
* @param jobId Unique integer used to identify this job.
* @param notificationId The ID for this notification, as per
* {@link android.app.NotificationManager#notify(int, Notification)}.
* @param notification The notification to be displayed.
* @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
*/
void setNotification(int jobId, int notificationId,
in Notification notification, int jobEndNotificationPolicy);
}

View File

@@ -19,13 +19,18 @@ package android.app.job;
import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
import android.annotation.BytesLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.Service;
import android.compat.Compatibility;
import android.content.Intent;
import android.os.IBinder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p>
* <p>This is the base class that handles asynchronous requests that were previously scheduled. You
@@ -63,6 +68,32 @@ public abstract class JobService extends Service {
public static final String PERMISSION_BIND =
"android.permission.BIND_JOB_SERVICE";
/**
* Detach the notification supplied to
* {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
* The notification will remain shown even after JobScheduler stops the job.
*
* @hide
*/
public static final int JOB_END_NOTIFICATION_POLICY_DETACH = 0;
/**
* Cancel and remove the notification supplied to
* {@link #setNotification(JobParameters, int, Notification, int)} when the job ends.
* The notification will be removed from the notification shade.
*
* @hide
*/
public static final int JOB_END_NOTIFICATION_POLICY_REMOVE = 1;
/** @hide */
@IntDef(prefix = {"JOB_END_NOTIFICATION_POLICY_"}, value = {
JOB_END_NOTIFICATION_POLICY_DETACH,
JOB_END_NOTIFICATION_POLICY_REMOVE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface JobEndNotificationPolicy {
}
private JobServiceEngine mEngine;
/** @hide */
@@ -364,4 +395,37 @@ public abstract class JobService extends Service {
}
return 0;
}
/**
* Provide JobScheduler with a notification to post and tie to this job's lifecycle.
* This is required for all user-initiated jobs
* (scheduled via {link JobInfo.Builder#setUserInitiated(boolean)}) and optional for
* other jobs. If the app does not call this method for a required notification within
* 10 seconds after {@link #onStartJob(JobParameters)} is called,
* the system will trigger an ANR and stop this job.
*
* <p>
* Note that certain types of jobs
* (e.g. {@link JobInfo.Builder#setDataTransfer data transfer jobs}) may require the
* notification to have certain characteristics and their documentation will state
* any such requirements.
*
* <p>
* JobScheduler will not remember this notification after the job has finished running,
* so apps must call this every time the job is started (if required or desired).
*
* @param params The parameters identifying this job, as supplied to
* the job in the {@link #onStartJob(JobParameters)} callback.
* @param notificationId The ID for this notification, as per
* {@link android.app.NotificationManager#notify(int,
* Notification)}.
* @param notification The notification to be displayed.
* @param jobEndNotificationPolicy The policy to apply to the notification when the job stops.
* @hide
*/
public final void setNotification(@NonNull JobParameters params, int notificationId,
@NonNull Notification notification,
@JobEndNotificationPolicy int jobEndNotificationPolicy) {
mEngine.setNotification(params, notificationId, notification, jobEndNotificationPolicy);
}
}

View File

@@ -21,6 +21,7 @@ import static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEM
import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.Service;
import android.compat.Compatibility;
import android.content.Intent;
@@ -73,6 +74,8 @@ public abstract class JobServiceEngine {
private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
/** Message that the client wants to update JobScheduler of the estimated transfer size. */
private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
/** Message that the client wants to give JobScheduler a notification to tie to the job. */
private static final int MSG_SET_NOTIFICATION = 7;
private final IJobService mBinder;
@@ -250,6 +253,24 @@ public abstract class JobServiceEngine {
args.recycle();
break;
}
case MSG_SET_NOTIFICATION: {
final SomeArgs args = (SomeArgs) msg.obj;
final JobParameters params = (JobParameters) args.arg1;
final Notification notification = (Notification) args.arg2;
IJobCallback callback = params.getCallback();
if (callback != null) {
try {
callback.setNotification(params.getJobId(),
args.argi1, notification, args.argi2);
} catch (RemoteException e) {
Log.e(TAG, "Error providing notification: binder has gone away.");
}
} else {
Log.e(TAG, "setNotification() called for a nonexistent job.");
}
args.recycle();
break;
}
default:
Log.e(TAG, "Unrecognised message received.");
break;
@@ -432,4 +453,27 @@ public abstract class JobServiceEngine {
args.argl2 = uploadBytes;
mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
}
}
/**
* Give JobScheduler a notification to tie to this job's lifecycle.
*
* @hide
* @see JobService#setNotification(JobParameters, int, Notification, int)
*/
public void setNotification(@NonNull JobParameters params, int notificationId,
@NonNull Notification notification,
@JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
if (params == null) {
throw new NullPointerException("params");
}
if (notification == null) {
throw new NullPointerException("notification");
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = params;
args.arg2 = notification;
args.argi1 = notificationId;
args.argi2 = jobEndNotificationPolicy;
mHandler.obtainMessage(MSG_SET_NOTIFICATION, args).sendToTarget();
}
}

View File

@@ -17,6 +17,8 @@
package com.android.server.job;
import static android.app.job.JobInfo.getPriorityString;
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
@@ -24,6 +26,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import android.annotation.BytesLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.job.IJobCallback;
import android.app.job.IJobService;
import android.app.job.JobInfo;
@@ -58,6 +61,7 @@ import com.android.internal.util.FrameworkStatsLog;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
import com.android.server.notification.NotificationManagerInternal;
import com.android.server.tare.EconomicPolicy;
import com.android.server.tare.EconomyManagerInternal;
import com.android.server.tare.JobSchedulerEconomicPolicy;
@@ -117,6 +121,7 @@ public final class JobServiceContext implements ServiceConnection {
private final EconomyManagerInternal mEconomyManagerInternal;
private final JobPackageTracker mJobPackageTracker;
private final PowerManager mPowerManager;
private final NotificationManagerInternal mNotificationManagerInternal;
private PowerManager.WakeLock mWakeLock;
// Execution state.
@@ -169,6 +174,11 @@ public final class JobServiceContext implements ServiceConnection {
/** The absolute maximum amount of time the job can run */
private long mMaxExecutionTimeMillis;
private int mNotificationId;
private Notification mNotification;
private int mNotificationPid;
private int mNotificationJobStopPolicy;
/**
* The stop reason for a pending cancel. If there's not pending cancel, then the value should be
* {@link JobParameters#STOP_REASON_UNDEFINED}.
@@ -235,6 +245,12 @@ public final class JobServiceContext implements ServiceConnection {
long downloadBytes, long uploadBytes) {
doUpdateTransferredNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
}
@Override
public void setNotification(int jobId, int notificationId,
Notification notification, int jobEndNotificationPolicy) {
doSetNotification(this, jobId, notificationId, notification, jobEndNotificationPolicy);
}
}
JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
@@ -244,6 +260,7 @@ public final class JobServiceContext implements ServiceConnection {
mService = service;
mBatteryStats = batteryStats;
mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
mJobPackageTracker = tracker;
mCallbackHandler = new JobServiceHandler(looper);
mJobConcurrencyManager = concurrencyManager;
@@ -593,6 +610,49 @@ public final class JobServiceContext implements ServiceConnection {
}
}
private void doSetNotification(JobCallback cb, int jodId, int notificationId,
Notification notification, int jobEndNotificationPolicy) {
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
if (!verifyCallerLocked(cb)) {
return;
}
if (callingUid != mRunningJob.getUid()) {
Slog.wtfStack(TAG, "Calling UID isn't the same as running job's UID...");
throw new SecurityException("Can't post notification on behalf of another app");
}
if (notification == null) {
throw new NullPointerException("notification");
}
if (notification.getSmallIcon() == null) {
throw new IllegalArgumentException("small icon required");
}
final String callingPkgName = mRunningJob.getServiceComponent().getPackageName();
if (null == mNotificationManagerInternal.getNotificationChannel(
callingPkgName, callingUid, notification.getChannelId())) {
throw new IllegalArgumentException("invalid notification channel");
}
if (jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_DETACH
&& jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_REMOVE) {
throw new IllegalArgumentException("invalid job end notification policy");
}
// TODO(260848384): ensure apps can't cancel the notification for user-initiated job
mNotificationManagerInternal.enqueueNotification(
callingPkgName, callingPkgName, callingUid, callingPid, /* tag */ null,
notificationId, notification, UserHandle.getUserId(callingUid));
mNotificationId = notificationId;
mNotification = notification;
mNotificationPid = callingPid;
mNotificationJobStopPolicy = jobEndNotificationPolicy;
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId,
@Nullable JobWorkItem item, long downloadBytes, long uploadBytes) {
// TODO(255393346): Make sure apps call this appropriately and monitor for abuse
@@ -1116,6 +1176,13 @@ public final class JobServiceContext implements ServiceConnection {
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
String.valueOf(mRunningJob.getJobId()));
}
if (mNotification != null
&& mNotificationJobStopPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE) {
final String callingPkgName = completedJob.getServiceComponent().getPackageName();
mNotificationManagerInternal.cancelNotification(
callingPkgName, callingPkgName, completedJob.getUid(), mNotificationPid,
/* tag */ null, mNotificationId, UserHandle.getUserId(completedJob.getUid()));
}
if (mWakeLock != null) {
mWakeLock.release();
}
@@ -1133,6 +1200,7 @@ public final class JobServiceContext implements ServiceConnection {
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
mPendingInternalStopReason = 0;
mPendingDebugStopReason = null;
mNotification = null;
removeOpTimeOutLocked();
mCompletedListener.onJobCompletedLocked(completedJob, internalStopReason, reschedule);
mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
@@ -1157,6 +1225,7 @@ public final class JobServiceContext implements ServiceConnection {
private void scheduleOpTimeOutLocked() {
removeOpTimeOutLocked();
// TODO(260848384): enforce setNotification timeout for user-initiated jobs
final long timeoutMillis;
switch (mVerb) {
case VERB_EXECUTING: