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

Commit cd8d4b7a authored by Alexander Roederer's avatar Alexander Roederer
Browse files

Ensure listeners are alerted to notif after LE

When a notification is posted, checks both the old _and_ new
notifications for the lifetime extension flag. If both have the flag,
then we know that this is the sysui-specific update occuring on a direct
reply. In this case, we don't need to alert any other listeners about
the update.

Previous versions of this code would only check the old notification for
the flag. On an update after a direct reply, the old notification would
have the flag. This would lead to the prepareNotifyPostedLocked function
hitting the "break;" statement as soon as system UI was alerted about
the notification, which meant that other listeners (like Wear) would not
be alerted.

Bug: 352471135
Flag: android.app.lifetime_extension_refactor
Test: atest NotificationListenersTest, flash and test
Change-Id: I036601a66ee94baa60e36217a1c392aee4bed5ab
parent 3b0072a7
Loading
Loading
Loading
Loading
+23 −10
Original line number Diff line number Diff line
@@ -12577,18 +12577,31 @@ public class NotificationManagerService extends SystemService {
                        // Checks if this is a request to notify system UI about a notification that
                        // has been lifetime extended.
                        // (We only need to check old for the flag, because in both cancellation and
                        // update cases, old should have the flag, whereas in update cases the
                        // new will NOT have the flag.)
                        // If it is such a request, and this is system UI, we send the post request
                        // only to System UI, and break as we don't need to continue checking other
                        // Managed Services.
                        if (info.isSystemUi() && old != null && old.getNotification() != null
                        // We check both old and new for the flag, to avoid catching updates
                        // (where new will not have the flag).
                        // If it is such a request, and this is the system UI listener, we send
                        // the post request. If it's any other listener, we skip it.
                        if (old != null && old.getNotification() != null
                                && (old.getNotification().flags
                                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
                                && sbn != null && sbn.getNotification() != null
                                && (sbn.getNotification().flags
                                & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
                            final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
                            if (info.isSystemUi()) {
                                final NotificationRankingUpdate update =
                                        makeRankingUpdateLocked(info);
                                listenerCalls.add(() -> notifyPosted(info, sbnToPost, update));
                                break;
                            } else {
                                // Skipping because this is the direct-reply "update" and we only
                                // need to send it to sysui, so we immediately continue, before it
                                // can get sent to other listeners below.
                                if (DBG) {
                                    Slog.d(TAG, "prepareNotifyPostedLocked: direct reply update, "
                                            + "skipping post to " + info.toString());
                                }
                                continue;
                            }
                        }
                    }
+209 −10
Original line number Diff line number Diff line
@@ -835,7 +835,7 @@ public class NotificationListenersTest extends UiServiceTestCase {
    }

    @Test
    public void testListenerPost_UpdateLifetimeExtended() throws Exception {
    public void testListenerPostLifetimeExtended_UpdatesOnlySysui() throws Exception {
        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);

        // Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
@@ -856,31 +856,51 @@ public class NotificationListenersTest extends UiServiceTestCase {
        Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
                .setContentTitle("new title")
                .setSmallIcon(android.R.drawable.sym_def_app_icon)
                .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
                .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
        StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
                nb2.build(), userHandle, null, 0);
        NotificationRecord toPost = new NotificationRecord(mContext, sbn2, channel);

        // Create system ui-like service.
        ManagedServices.ManagedServiceInfo info = mListeners.new ManagedServiceInfo(
        ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
                null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
        info.isSystemUi = true;
        INotificationListener l1 = mock(INotificationListener.class);
        info.service = l1;
        List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(info);
        sysuiInfo.isSystemUi = true;
        INotificationListener sysuiListener = mock(INotificationListener.class);
        sysuiInfo.service = sysuiListener;

        // Create two non-system ui-like services.
        ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
                null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
        otherInfo1.isSystemUi = false;
        INotificationListener otherListener1 = mock(INotificationListener.class);
        otherInfo1.service = otherListener1;

        ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
                null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
        otherInfo2.isSystemUi = false;
        INotificationListener otherListener2 = mock(INotificationListener.class);
        otherInfo2.service = otherListener2;

        List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
                otherInfo2);
        when(mListeners.getServices()).thenReturn(services);

        FieldSetter.setField(mNm,
                NotificationManagerService.class.getDeclaredField("mHandler"),
                mock(NotificationManagerService.WorkerHandler.class));
        doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm).makeRankingUpdateLocked(info);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(sysuiInfo);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(otherInfo1);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(otherInfo2);
        doReturn(false).when(mNm).isInLockDownMode(anyInt());
        doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
        doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
        doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());

        // The notification change is posted to the service listener.
        // Post notification change to the service listeners.
        mListeners.notifyPostedLocked(toPost, old);

        // Verify that the post occcurs with the updated notification value.
@@ -889,11 +909,190 @@ public class NotificationListenersTest extends UiServiceTestCase {
        runnableCaptor.getValue().run();
        ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
                ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
        verify(l1, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
        verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
        StatusBarNotification sbnResult = sbnCaptor.getValue().get();
        assertThat(sbnResult.getNotification()
                .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
                .isEqualTo("new title");

        verify(otherListener1, never()).onNotificationPosted(any(), any());
        verify(otherListener2, never()).onNotificationPosted(any(), any());
    }

    @Test
    public void testListenerPostLifeimteExtension_postsToAppropriateListeners() throws Exception {
        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);

        // Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
        String pkg = "pkg";
        int uid = 9;
        UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
        NotificationChannel channel = new NotificationChannel("id", "name",
                NotificationManager.IMPORTANCE_HIGH);
        Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
                .setContentTitle("foo")
                .setSmallIcon(android.R.drawable.sym_def_app_icon)
                .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
        StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
                nb.build(), userHandle, null, 0);
        NotificationRecord leRecord = new NotificationRecord(mContext, sbn, channel);

        // Creates updated notification (without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY)
        Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
                .setContentTitle("new title")
                .setSmallIcon(android.R.drawable.sym_def_app_icon)
                .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
        StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
                nb2.build(), userHandle, null, 0);
        NotificationRecord nonLeRecord = new NotificationRecord(mContext, sbn2, channel);

        // Create system ui-like service.
        ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
                null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
        sysuiInfo.isSystemUi = true;
        INotificationListener sysuiListener = mock(INotificationListener.class);
        sysuiInfo.service = sysuiListener;

        // Create two non-system ui-like services.
        ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
                null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
        otherInfo1.isSystemUi = false;
        INotificationListener otherListener1 = mock(INotificationListener.class);
        otherInfo1.service = otherListener1;

        ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
                null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
        otherInfo2.isSystemUi = false;
        INotificationListener otherListener2 = mock(INotificationListener.class);
        otherInfo2.service = otherListener2;

        List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
                otherInfo2);
        when(mListeners.getServices()).thenReturn(services);

        FieldSetter.setField(mNm,
                NotificationManagerService.class.getDeclaredField("mHandler"),
                mock(NotificationManagerService.WorkerHandler.class));
        doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(sysuiInfo);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(otherInfo1);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(otherInfo2);
        doReturn(false).when(mNm).isInLockDownMode(anyInt());
        doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
        doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
        doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());

        // The notification change is posted to the service listener.
        // NonLE to LE should never happen, as LE can't be set in an update by the app.
        // So we just want to test LE to NonLE.
        mListeners.notifyPostedLocked(nonLeRecord /*=toPost*/, leRecord /*=old*/);

        // Verify that the post occcurs with the updated notification value.
        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
        verify(mNm.mHandler, times(3)).post(runnableCaptor.capture());
        List<Runnable> capturedRunnable = runnableCaptor.getAllValues();
        for (Runnable r : capturedRunnable) {
            r.run();
        }

        ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
                ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
        verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
        StatusBarNotification sbnResult = sbnCaptor.getValue().get();
        assertThat(sbnResult.getNotification()
                .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
                .isEqualTo("new title");

        verify(otherListener1, times(1)).onNotificationPosted(any(), any());
        verify(otherListener2, times(1)).onNotificationPosted(any(), any());
    }

    @Test
    public void testNotifyPostedLocked_postsToAppropriateListeners() throws Exception {
        // Create original notification
        String pkg = "pkg";
        int uid = 9;
        UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
        NotificationChannel channel = new NotificationChannel("id", "name",
                NotificationManager.IMPORTANCE_HIGH);
        Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
                .setContentTitle("foo")
                .setSmallIcon(android.R.drawable.sym_def_app_icon);
        StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
                nb.build(), userHandle, null, 0);
        NotificationRecord oldRecord = new NotificationRecord(mContext, sbn, channel);

        // Creates updated notification
        Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
                .setContentTitle("new title")
                .setSmallIcon(android.R.drawable.sym_def_app_icon);
        StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
                nb2.build(), userHandle, null, 0);
        NotificationRecord newRecord = new NotificationRecord(mContext, sbn2, channel);

        // Create system ui-like service.
        ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
                null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
        sysuiInfo.isSystemUi = true;
        INotificationListener sysuiListener = mock(INotificationListener.class);
        sysuiInfo.service = sysuiListener;

        // Create two non-system ui-like services.
        ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
                null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
        otherInfo1.isSystemUi = false;
        INotificationListener otherListener1 = mock(INotificationListener.class);
        otherInfo1.service = otherListener1;

        ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
                null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
        otherInfo2.isSystemUi = false;
        INotificationListener otherListener2 = mock(INotificationListener.class);
        otherInfo2.service = otherListener2;

        List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
                otherInfo2);
        when(mListeners.getServices()).thenReturn(services);

        FieldSetter.setField(mNm,
                NotificationManagerService.class.getDeclaredField("mHandler"),
                mock(NotificationManagerService.WorkerHandler.class));
        doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(sysuiInfo);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(otherInfo1);
        doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
                .makeRankingUpdateLocked(otherInfo2);
        doReturn(false).when(mNm).isInLockDownMode(anyInt());
        doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
        doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
        doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());

        // The notification change is posted to the service listeners.
        mListeners.notifyPostedLocked(newRecord, oldRecord);

        // Verify that the post occcurs with the updated notification value.
        ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
        verify(mNm.mHandler, times(3)).post(runnableCaptor.capture());
        List<Runnable> capturedRunnable = runnableCaptor.getAllValues();
        for (Runnable r : capturedRunnable) {
            r.run();
        }

        ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
                ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
        verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
        StatusBarNotification sbnResult = sbnCaptor.getValue().get();
        assertThat(sbnResult.getNotification()
                .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
                .isEqualTo("new title");

        verify(otherListener1, times(1)).onNotificationPosted(any(), any());
        verify(otherListener2, times(1)).onNotificationPosted(any(), any());
    }

    /**