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

Commit 5576565e authored by Varun Shah's avatar Varun Shah
Browse files

Restrict apps from dismissing UIJ notifications.

Apps should not be able to dismiss notifications associated with
user-initiated jobs nor should they be able to delete the associated
notification channel.

Bug: 269533590
Test: atest NotificationManagerTest
Test: atest NotificationManagerServiceTest
Test: atest ServiceTest
Test: atest NotificationChannelTest
Test: atest NotificationTest
Test: atest JobNotificationCoordinatorTest
Change-Id: I59d95120b3722ed8cb6675ed89576b05de5e6092
parent 9de342ab
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.job;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -58,6 +59,19 @@ public interface JobSchedulerInternal {
     */
    void reportAppUsage(String packageName, int userId);

    /**
     * @return {@code true} if the given notification is associated with any user-initiated jobs.
     */
    boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId,
            int userId, @NonNull String packageName);

    /**
     * @return {@code true} if the given notification channel is associated with any user-initiated
     * jobs.
     */
    boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(String notificationChannel,
            int userId, String packageName);

    /**
     * Report a snapshot of sync-related jobs back to the sync manager
     */
+14 −0
Original line number Diff line number Diff line
@@ -1925,6 +1925,20 @@ class JobConcurrencyManager {
        return null;
    }

    @GuardedBy("mLock")
    boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId, int userId,
            String packageName) {
        return mNotificationCoordinator.isNotificationAssociatedWithAnyUserInitiatedJobs(
                notificationId, userId, packageName);
    }

    @GuardedBy("mLock")
    boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(String notificationChannel,
            int userId, String packageName) {
        return mNotificationCoordinator.isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
                notificationChannel, userId, packageName);
    }

    @NonNull
    private JobServiceContext createNewJobServiceContext() {
        return mInjector.createJobServiceContext(mService, this, mNotificationCoordinator,
+71 −9
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.util.Slog;
import android.util.SparseSetArray;

import com.android.server.LocalServices;
import com.android.server.job.controllers.JobStatus;
import com.android.server.notification.NotificationManagerInternal;

class JobNotificationCoordinator {
@@ -52,16 +53,18 @@ class JobNotificationCoordinator {
        @NonNull
        public final UserPackage userPackage;
        public final int notificationId;
        public final String notificationChannel;
        public final int appPid;
        public final int appUid;
        @JobService.JobEndNotificationPolicy
        public final int jobEndNotificationPolicy;

        NotificationDetails(@NonNull UserPackage userPackage, int appPid, int appUid,
                int notificationId,
                int notificationId, String notificationChannel,
                @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
            this.userPackage = userPackage;
            this.notificationId = notificationId;
            this.notificationChannel = notificationChannel;
            this.appPid = appPid;
            this.appUid = appUid;
            this.jobEndNotificationPolicy = jobEndNotificationPolicy;
@@ -84,14 +87,14 @@ class JobNotificationCoordinator {
            removeNotificationAssociation(hostingContext, JobParameters.STOP_REASON_UNDEFINED);
        }
        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 JobStatus jobStatus = hostingContext.getRunningJobLocked();
        if (jobStatus != null && jobStatus.startedAsUserInitiatedJob) {
            notification.flags |= Notification.FLAG_USER_INITIATED_JOB;
        }
        final UserPackage userPackage = UserPackage.of(userId, packageName);
        final NotificationDetails details = new NotificationDetails(
                userPackage, callingPid, callingUid, notificationId, jobEndNotificationPolicy);
                userPackage, callingPid, callingUid, notificationId, notification.getChannelId(),
                jobEndNotificationPolicy);
        SparseSetArray<JobServiceContext> appNotifications = mCurrentAssociations.get(userPackage);
        if (appNotifications == null) {
            appNotifications = new SparseSetArray<>();
@@ -99,6 +102,11 @@ class JobNotificationCoordinator {
        }
        appNotifications.add(notificationId, hostingContext);
        mNotificationDetails.put(hostingContext, details);
        // Call into NotificationManager after internal data structures have been updated since
        // NotificationManager calls into this class to check for any existing associations.
        mNotificationManagerInternal.enqueueNotification(
                packageName, packageName, callingUid, callingPid, /* tag */ null,
                notificationId, notification, userId);
    }

    void removeNotificationAssociation(@NonNull JobServiceContext hostingContext,
@@ -113,6 +121,9 @@ class JobNotificationCoordinator {
            Slog.wtf(TAG, "Association data structures not in sync");
            return;
        }
        final String packageName = details.userPackage.packageName;
        final int userId = UserHandle.getUserId(details.appUid);
        boolean stripUijFlag = true;
        ArraySet<JobServiceContext> associatedContexts = associations.get(details.notificationId);
        if (associatedContexts == null || associatedContexts.isEmpty()) {
            // No more jobs using this notification. Apply the final job stop policy.
@@ -120,14 +131,65 @@ class JobNotificationCoordinator {
            // so the user doesn't get confused about the app state.
            if (details.jobEndNotificationPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE
                    || stopReason == JobParameters.STOP_REASON_USER) {
                final String packageName = details.userPackage.packageName;
                mNotificationManagerInternal.cancelNotification(
                        packageName, packageName, details.appUid, details.appPid, /* tag */ null,
                        details.notificationId, UserHandle.getUserId(details.appUid));
                        details.notificationId, userId);
                stripUijFlag = false;
            }
        } else {
            // Strip the UIJ flag only if there are no other UIJs associated with the notification
            stripUijFlag = !isNotificationAssociatedWithAnyUserInitiatedJobs(
                    details.notificationId, userId, packageName);
        }
        if (stripUijFlag) {
            // Strip the user-initiated job flag from the notification.
            mNotificationManagerInternal.removeUserInitiatedJobFlagFromNotification(
                    packageName, details.notificationId, userId);
        }
    }

    boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId,
            int userId, String packageName) {
        final UserPackage pkgDetails = UserPackage.of(userId, packageName);
        final SparseSetArray<JobServiceContext> associations = mCurrentAssociations.get(pkgDetails);
        if (associations == null) {
            return false;
        }
        final ArraySet<JobServiceContext> associatedContexts = associations.get(notificationId);
        if (associatedContexts == null) {
            return false;
        }

        // Check if any UIJs associated with this package are using the same notification
        for (int i = associatedContexts.size() - 1; i >= 0; i--) {
            final JobStatus jobStatus = associatedContexts.valueAt(i).getRunningJobLocked();
            if (jobStatus != null && jobStatus.startedAsUserInitiatedJob) {
                return true;
            }
        }
        return false;
    }

    boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(String notificationChannel,
            int userId, String packageName) {
        for (int i = mNotificationDetails.size() - 1; i >= 0; i--) {
            final JobServiceContext jsc = mNotificationDetails.keyAt(i);
            final NotificationDetails details = mNotificationDetails.get(jsc);
            // Check if the details for the given notification match and if the associated job
            // was started as a user initiated job
            if (details != null
                    && UserHandle.getUserId(details.appUid) == userId
                    && details.userPackage.packageName.equals(packageName)
                    && details.notificationChannel.equals(notificationChannel)) {
                final JobStatus jobStatus = jsc.getRunningJobLocked();
                if (jobStatus != null && jobStatus.startedAsUserInitiatedJob) {
                    return true;
                }
            }
        }
        return false;
    }

    private void validateNotification(@NonNull String packageName, int callingUid,
            @NonNull Notification notification,
            @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
+24 −0
Original line number Diff line number Diff line
@@ -3711,6 +3711,30 @@ public class JobSchedulerService extends com.android.server.SystemService
            JobSchedulerService.this.reportAppUsage(packageName, userId);
        }

        @Override
        public boolean isNotificationAssociatedWithAnyUserInitiatedJobs(int notificationId,
                int userId, String packageName) {
            if (packageName == null) {
                return false;
            }
            synchronized (mLock) {
                return mConcurrencyManager.isNotificationAssociatedWithAnyUserInitiatedJobs(
                        notificationId, userId, packageName);
            }
        }

        @Override
        public boolean isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
                String notificationChannel, int userId, String packageName) {
            if (packageName == null || notificationChannel == null) {
                return false;
            }
            synchronized (mLock) {
                return mConcurrencyManager.isNotificationChannelAssociatedWithAnyUserInitiatedJobs(
                        notificationChannel, userId, packageName);
            }
        }

        @Override
        public JobStorePersistStats getPersistStats() {
            synchronized (mLock) {
+3 −1
Original line number Diff line number Diff line
@@ -338,10 +338,12 @@ package android.app {
  }

  public class Notification implements android.os.Parcelable {
    method public boolean isUserInitiatedJob();
    method public boolean shouldShowForegroundImmediately();
    field public static final String EXTRA_MEDIA_REMOTE_DEVICE = "android.mediaRemoteDevice";
    field public static final String EXTRA_MEDIA_REMOTE_ICON = "android.mediaRemoteIcon";
    field public static final String EXTRA_MEDIA_REMOTE_INTENT = "android.mediaRemoteIntent";
    field public static final int FLAG_USER_INITIATED_JOB = 32768; // 0x8000
  }

  public final class NotificationChannel implements android.os.Parcelable {
@@ -351,10 +353,10 @@ package android.app {
    method public void setDeleted(boolean);
    method public void setDeletedTimeMs(long);
    method public void setDemoted(boolean);
    method public void setFgServiceShown(boolean);
    method public void setImportanceLockedByCriticalDeviceFunction(boolean);
    method public void setImportantConversation(boolean);
    method public void setOriginalImportance(int);
    method public void setUserVisibleTaskShown(boolean);
  }

  public final class NotificationChannelGroup implements android.os.Parcelable {
Loading