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

Commit 3ed5cc4c authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Introduce centralized notification coordinator."

parents bd51bfb8 9fc422b2
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -416,6 +416,11 @@ public abstract class JobService extends Service {
     * 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).
     *
     * <p>
     * If separate jobs use the same notification ID with this API, the most recently provided
     * notification will be shown to the user, and the
     * {@code jobEndNotificationPolicy} of the last job to stop will be applied.
     *
     * @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
+9 −5
Original line number Diff line number Diff line
@@ -193,6 +193,7 @@ class JobConcurrencyManager {
    }

    private final Object mLock;
    private final JobNotificationCoordinator mNotificationCoordinator;
    private final JobSchedulerService mService;
    private final Context mContext;
    private final Handler mHandler;
@@ -418,6 +419,7 @@ class JobConcurrencyManager {
        mLock = mService.getLock();
        mContext = service.getTestableContext();
        mInjector = injector;
        mNotificationCoordinator = new JobNotificationCoordinator();

        mHandler = JobSchedulerBackgroundThread.getHandler();

@@ -451,7 +453,8 @@ class JobConcurrencyManager {
                ServiceManager.getService(BatteryStats.SERVICE_NAME));
        for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) {
            mIdleContexts.add(
                    mInjector.createJobServiceContext(mService, this, batteryStats,
                    mInjector.createJobServiceContext(mService, this,
                            mNotificationCoordinator, batteryStats,
                            mService.mJobPackageTracker, mContext.getMainLooper()));
        }
    }
@@ -1687,7 +1690,7 @@ class JobConcurrencyManager {

    @NonNull
    private JobServiceContext createNewJobServiceContext() {
        return mInjector.createJobServiceContext(mService, this,
        return mInjector.createJobServiceContext(mService, this, mNotificationCoordinator,
                IBatteryStats.Stub.asInterface(
                        ServiceManager.getService(BatteryStats.SERVICE_NAME)),
                mService.mJobPackageTracker, mContext.getMainLooper());
@@ -2612,10 +2615,11 @@ class JobConcurrencyManager {
    static class Injector {
        @NonNull
        JobServiceContext createJobServiceContext(JobSchedulerService service,
                JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
                JobConcurrencyManager concurrencyManager,
                JobNotificationCoordinator notificationCoordinator, IBatteryStats batteryStats,
                JobPackageTracker tracker, Looper looper) {
            return new JobServiceContext(service, concurrencyManager, batteryStats,
                    tracker, looper);
            return new JobServiceContext(service, concurrencyManager, notificationCoordinator,
                    batteryStats, tracker, looper);
        }
    }
}
+144 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.job;

import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;

import android.annotation.NonNull;
import android.app.Notification;
import android.app.job.JobService;
import android.content.pm.UserPackage;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseSetArray;

import com.android.server.LocalServices;
import com.android.server.notification.NotificationManagerInternal;

class JobNotificationCoordinator {
    private static final String TAG = "JobNotificationCoordinator";

    /**
     * Mapping of UserPackage -> {notificationId -> List<JobServiceContext>} to track which jobs
     * are associated with each app's notifications.
     */
    private final ArrayMap<UserPackage, SparseSetArray<JobServiceContext>> mCurrentAssociations =
            new ArrayMap<>();
    /**
     * Set of NotificationDetails for each running job.
     */
    private final ArrayMap<JobServiceContext, NotificationDetails> mNotificationDetails =
            new ArrayMap<>();

    private static final class NotificationDetails {
        @NonNull
        public final UserPackage userPackage;
        public final int notificationId;
        public final int appPid;
        public final int appUid;
        @JobService.JobEndNotificationPolicy
        public final int jobEndNotificationPolicy;

        NotificationDetails(@NonNull UserPackage userPackage, int appPid, int appUid,
                int notificationId,
                @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
            this.userPackage = userPackage;
            this.notificationId = notificationId;
            this.appPid = appPid;
            this.appUid = appUid;
            this.jobEndNotificationPolicy = jobEndNotificationPolicy;
        }
    }

    private final NotificationManagerInternal mNotificationManagerInternal;

    JobNotificationCoordinator() {
        mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
    }

    void enqueueNotification(@NonNull JobServiceContext hostingContext, @NonNull String packageName,
            int callingPid, int callingUid, int notificationId, @NonNull Notification notification,
            @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
        validateNotification(packageName, callingUid, notification, jobEndNotificationPolicy);
        final NotificationDetails oldDetails = mNotificationDetails.get(hostingContext);
        if (oldDetails != null && oldDetails.notificationId != notificationId) {
            // App is switching notification IDs. Remove association with the old one.
            removeNotificationAssociation(hostingContext);
        }
        final int userId = UserHandle.getUserId(callingUid);
        // TODO(260848384): ensure apps can't cancel the notification for user-initiated job
        //       eg., by calling NotificationManager.cancel/All or deleting the notification channel
        mNotificationManagerInternal.enqueueNotification(
                packageName, packageName, callingUid, callingPid, /* tag */ null,
                notificationId, notification, userId);
        final UserPackage userPackage = UserPackage.of(userId, packageName);
        final NotificationDetails details = new NotificationDetails(
                userPackage, callingPid, callingUid, notificationId, jobEndNotificationPolicy);
        SparseSetArray<JobServiceContext> appNotifications = mCurrentAssociations.get(userPackage);
        if (appNotifications == null) {
            appNotifications = new SparseSetArray<>();
            mCurrentAssociations.put(userPackage, appNotifications);
        }
        appNotifications.add(notificationId, hostingContext);
        mNotificationDetails.put(hostingContext, details);
    }

    void removeNotificationAssociation(@NonNull JobServiceContext hostingContext) {
        final NotificationDetails details = mNotificationDetails.remove(hostingContext);
        if (details == null) {
            return;
        }
        final SparseSetArray<JobServiceContext> associations =
                mCurrentAssociations.get(details.userPackage);
        if (associations == null || !associations.remove(details.notificationId, hostingContext)) {
            Slog.wtf(TAG, "Association data structures not in sync");
            return;
        }
        ArraySet<JobServiceContext> associatedContexts = associations.get(details.notificationId);
        if (associatedContexts == null || associatedContexts.isEmpty()) {
            // No more jobs using this notification. Apply the final job stop policy.
            if (details.jobEndNotificationPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE) {
                final String packageName = details.userPackage.packageName;
                mNotificationManagerInternal.cancelNotification(
                        packageName, packageName, details.appUid, details.appPid, /* tag */ null,
                        details.notificationId, UserHandle.getUserId(details.appUid));
            }
        }
    }

    private void validateNotification(@NonNull String packageName, int callingUid,
            @NonNull Notification notification,
            @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
        if (notification == null) {
            throw new NullPointerException("notification");
        }
        if (notification.getSmallIcon() == null) {
            throw new IllegalArgumentException("small icon required");
        }
        if (null == mNotificationManagerInternal.getNotificationChannel(
                packageName, 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");
        }
    }
}
+7 −40
Original line number Diff line number Diff line
@@ -17,8 +17,6 @@
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;
@@ -61,7 +59,6 @@ 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;
@@ -113,6 +110,7 @@ public final class JobServiceContext implements ServiceConnection {
    /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
    private final JobCompletedListener mCompletedListener;
    private final JobConcurrencyManager mJobConcurrencyManager;
    private final JobNotificationCoordinator mNotificationCoordinator;
    private final JobSchedulerService mService;
    /** Used for service binding, etc. */
    private final Context mContext;
@@ -121,7 +119,6 @@ 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.
@@ -174,11 +171,6 @@ 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}.
@@ -254,16 +246,17 @@ public final class JobServiceContext implements ServiceConnection {
    }

    JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
            JobNotificationCoordinator notificationCoordinator,
            IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) {
        mContext = service.getContext();
        mLock = service.getLock();
        mService = service;
        mBatteryStats = batteryStats;
        mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
        mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
        mJobPackageTracker = tracker;
        mCallbackHandler = new JobServiceHandler(looper);
        mJobConcurrencyManager = concurrencyManager;
        mNotificationCoordinator = notificationCoordinator;
        mCompletedListener = service;
        mPowerManager = mContext.getSystemService(PowerManager.class);
        mAvailable = true;
@@ -624,29 +617,10 @@ public final class JobServiceContext implements ServiceConnection {
                    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;
                mNotificationCoordinator.enqueueNotification(this, callingPkgName,
                        callingPid, callingUid, notificationId,
                        notification, jobEndNotificationPolicy);
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
@@ -1179,13 +1153,7 @@ 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()));
        }
        mNotificationCoordinator.removeNotificationAssociation(this);
        if (mWakeLock != null) {
            mWakeLock.release();
        }
@@ -1203,7 +1171,6 @@ public final class JobServiceContext implements ServiceConnection {
        mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
        mPendingInternalStopReason = 0;
        mPendingDebugStopReason = null;
        mNotification = null;
        removeOpTimeOutLocked();
        if (completedJob.isUserVisibleJob()) {
            mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
+2 −1
Original line number Diff line number Diff line
@@ -113,7 +113,8 @@ public final class JobConcurrencyManagerTest {

        @Override
        JobServiceContext createJobServiceContext(JobSchedulerService service,
                JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
                JobConcurrencyManager concurrencyManager,
                JobNotificationCoordinator notificationCoordinator, IBatteryStats batteryStats,
                JobPackageTracker tracker, Looper looper) {
            final JobServiceContext context = mock(JobServiceContext.class);
            doAnswer((Answer<Boolean>) invocationOnMock -> {
Loading