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

Commit a56db5b9 authored by Gaurav Gupta's avatar Gaurav Gupta
Browse files

Track rapid cancelling of notifications using an AppOp

Bug: 289080543
Test: Presubmit + manual testing + unit testing included
Change-Id: I138ce2b15be418dec90905bf2efd75e3bb907e27
parent 522b180d
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -119,6 +119,7 @@ import static android.service.notification.NotificationListenerService.Ranking.R
import static android.service.notification.NotificationListenerService.Ranking.RANKING_UNCHANGED;
import static android.service.notification.NotificationListenerService.TRIM_FULL;
import static android.service.notification.NotificationListenerService.TRIM_LIGHT;
import static android.view.contentprotection.flags.Flags.rapidClearNotificationsByListenerAppOpEnabled;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
@@ -414,6 +415,12 @@ public class NotificationManagerService extends SystemService {
    static final long SNOOZE_UNTIL_UNSPECIFIED = -1;
    /**
     *  The threshold, in milliseconds, to determine whether a notification has been
     * cleared too quickly.
     */
    private static final int NOTIFICATION_RAPID_CLEAR_THRESHOLD_MS = 5000;
    static final int INVALID_UID = -1;
    static final String ROOT_PKG = "root";
@@ -4817,9 +4824,12 @@ public class NotificationManagerService extends SystemService {
            final int callingUid = Binder.getCallingUid();
            final int callingPid = Binder.getCallingPid();
            final long identity = Binder.clearCallingIdentity();
            boolean notificationsRapidlyCleared = false;
            final String pkg;
            try {
                synchronized (mNotificationLock) {
                    final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
                    pkg = info.component.getPackageName();
                    // Cancellation reason. If the token comes from assistant, label the
                    // cancellation as coming from the assistant; default to LISTENER_CANCEL.
@@ -4838,11 +4848,19 @@ public class NotificationManagerService extends SystemService {
                                    !mUserProfiles.isCurrentProfile(userId)) {
                                continue;
                            }
                            notificationsRapidlyCleared = notificationsRapidlyCleared
                                    || isNotificationRecent(r);
                            cancelNotificationFromListenerLocked(info, callingUid, callingPid,
                                    r.getSbn().getPackageName(), r.getSbn().getTag(),
                                    r.getSbn().getId(), userId, reason);
                        }
                    } else {
                        for (NotificationRecord notificationRecord : mNotificationList) {
                            if (isNotificationRecent(notificationRecord)) {
                                notificationsRapidlyCleared = true;
                                break;
                            }
                        }
                        if (lifetimeExtensionRefactor()) {
                            cancelAllLocked(callingUid, callingPid, info.userid,
                                    REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
@@ -4855,11 +4873,23 @@ public class NotificationManagerService extends SystemService {
                        }
                    }
                }
                if (notificationsRapidlyCleared) {
                    mAppOps.noteOpNoThrow(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,
                            callingUid, pkg, /* attributionTag= */ null, /* message= */ null);
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
        private boolean isNotificationRecent(@NonNull NotificationRecord notificationRecord) {
            if (!rapidClearNotificationsByListenerAppOpEnabled()) {
                return false;
            }
            return notificationRecord.getFreshnessMs(System.currentTimeMillis())
                    < NOTIFICATION_RAPID_CLEAR_THRESHOLD_MS;
        }
        /**
         * Handle request from an approved listener to re-enable itself.
         *
+175 −0
Original line number Diff line number Diff line
@@ -970,6 +970,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        return new NotificationRecord(mContext, sbn, channel);
    }
    private NotificationRecord generateNotificationRecord(NotificationChannel channel,
            long postTime) {
        final StatusBarNotification sbn = generateSbn(PKG, mUid, postTime, 0);
        return new NotificationRecord(mContext, sbn, channel);
    }
    private NotificationRecord generateNotificationRecord(NotificationChannel channel, int userId) {
        return generateNotificationRecord(channel, 1, userId);
    }
@@ -13309,6 +13315,175 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
        assertThat(n.flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(0);
    }
    @Test
    public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne()
            throws RemoteException {
        mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
        // Create recent notification.
        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
                System.currentTimeMillis());
        mService.addNotification(nr1);
        // Create old notification.
        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr2);
        // Cancel specific notifications via listener.
        String[] keys = {nr1.getSbn().getKey(), nr2.getSbn().getKey()};
        mService.getBinderService().cancelNotificationsFromListener(null, keys);
        waitForIdle();
        // Notifications should not be active anymore.
        StatusBarNotification[] notifications = mBinderService.getActiveNotifications(PKG);
        assertThat(notifications).isEmpty();
        assertEquals(0, mService.getNotificationRecordCount());
        // Ensure cancel event is logged.
        verify(mAppOpsManager).noteOpNoThrow(
                AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, mUid, PKG, null, null);
    }
    @Test
    public void cancelNotificationsFromListener_rapidClear_old_cancelOne() throws RemoteException {
        mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
        // Create old notifications.
        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr1);
        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr2);
        // Cancel specific notifications via listener.
        String[] keys = {nr1.getSbn().getKey(), nr2.getSbn().getKey()};
        mService.getBinderService().cancelNotificationsFromListener(null, keys);
        waitForIdle();
        // Notifications should not be active anymore.
        StatusBarNotification[] notifications = mBinderService.getActiveNotifications(PKG);
        assertThat(notifications).isEmpty();
        assertEquals(0, mService.getNotificationRecordCount());
        // Ensure cancel event is not logged.
        verify(mAppOpsManager, never()).noteOpNoThrow(
                eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
                any(), any());
    }
    @Test
    public void cancelNotificationsFromListener_rapidClear_oldNew_cancelOne_flagDisabled()
            throws RemoteException {
        mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
                .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
        // Create recent notification.
        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
                System.currentTimeMillis());
        mService.addNotification(nr1);
        // Create old notification.
        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr2);
        // Cancel specific notifications via listener.
        String[] keys = {nr1.getSbn().getKey(), nr2.getSbn().getKey()};
        mService.getBinderService().cancelNotificationsFromListener(null, keys);
        waitForIdle();
        // Notifications should not be active anymore.
        StatusBarNotification[] notifications = mBinderService.getActiveNotifications(PKG);
        assertThat(notifications).isEmpty();
        assertEquals(0, mService.getNotificationRecordCount());
        // Ensure cancel event is not logged due to flag being disabled.
        verify(mAppOpsManager, never()).noteOpNoThrow(
                eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
                any(), any());
    }
    @Test
    public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll()
            throws RemoteException {
        mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
        // Create recent notification.
        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
                System.currentTimeMillis());
        mService.addNotification(nr1);
        // Create old notification.
        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr2);
        // Cancel all notifications via listener.
        mService.getBinderService().cancelNotificationsFromListener(null, null);
        waitForIdle();
        // Notifications should not be active anymore.
        StatusBarNotification[] notifications = mBinderService.getActiveNotifications(PKG);
        assertThat(notifications).isEmpty();
        assertEquals(0, mService.getNotificationRecordCount());
        // Ensure cancel event is logged.
        verify(mAppOpsManager).noteOpNoThrow(
                AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, mUid, PKG, null, null);
    }
    @Test
    public void cancelNotificationsFromListener_rapidClear_old_cancelAll() throws RemoteException {
        mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags
                .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
        // Create old notifications.
        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr1);
        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr2);
        // Cancel all notifications via listener.
        mService.getBinderService().cancelNotificationsFromListener(null, null);
        waitForIdle();
        // Notifications should not be active anymore.
        StatusBarNotification[] notifications = mBinderService.getActiveNotifications(PKG);
        assertThat(notifications).isEmpty();
        assertEquals(0, mService.getNotificationRecordCount());
        // Ensure cancel event is not logged.
        verify(mAppOpsManager, never()).noteOpNoThrow(
                eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
                any(), any());
    }
    @Test
    public void cancelNotificationsFromListener_rapidClear_oldNew_cancelAll_flagDisabled()
            throws RemoteException {
        mSetFlagsRule.disableFlags(android.view.contentprotection.flags.Flags
                .FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED);
        // Create recent notification.
        final NotificationRecord nr1 = generateNotificationRecord(mTestNotificationChannel,
                System.currentTimeMillis());
        mService.addNotification(nr1);
        // Create old notification.
        final NotificationRecord nr2 = generateNotificationRecord(mTestNotificationChannel, 0);
        mService.addNotification(nr2);
        // Cancel all notifications via listener.
        mService.getBinderService().cancelNotificationsFromListener(null, null);
        waitForIdle();
        // Notifications should not be active anymore.
        StatusBarNotification[] notifications = mBinderService.getActiveNotifications(PKG);
        assertThat(notifications).isEmpty();
        assertEquals(0, mService.getNotificationRecordCount());
        // Ensure cancel event is not logged due to flag being disabled.
        verify(mAppOpsManager, never()).noteOpNoThrow(
                eq(AppOpsManager.OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER), anyInt(), anyString(),
                any(), any());
    }
    private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
            throws RemoteException {
        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0,