Merge "Add API to allow apps to tie a notification to a job."
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user