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

Commit 4f88a4e3 authored by Gaurav Gupta's avatar Gaurav Gupta Committed by Android (Google) Code Review
Browse files

Merge "Track rapid cancelling of notifications using an AppOp" into main

parents 9d42d213 a56db5b9
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
@@ -974,6 +974,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);
    }
@@ -13440,6 +13446,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,