Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 57df5c3f authored by Kweku Adams's avatar Kweku Adams Committed by Android (Google) Code Review
Browse files

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

parents d0677677 03d7a4af
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -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);
}
+64 −0
Original line number Diff line number Diff line
@@ -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);
    }
}
+45 −1
Original line number Diff line number Diff line
@@ -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();
    }
}
+69 −0
Original line number Diff line number Diff line
@@ -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: