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

Commit 5e9f078d authored by Alexander Roederer's avatar Alexander Roederer
Browse files

Send update from NMS to SysUI on LifetimeExtension

When an app attempts to cancel a notification that has been lifetime
extended with the FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, we need to
notify SystemUI so that it can transition the notification from the
"sending" state to the "sent" state. To do this, we send a post only to
SystemUI, and add a new NotifCollectionListener to
RemoteInputCoordinator to watch for onEntryUpdated. When the entry is
updated, it rebuilds the notification for RemoteInputReply.

Flag: ACONFIG android.app.lifetimeExtensionRefactor DEVELOPMENT
Bug: 230652175
Test: atest RemoteInputCoordinatorTest ManagedServicesTest NotificationManagerServiceTest

Change-Id: Ib81885b73026e779f4af6a1bdd4399c677f37e8d
parent 1f66b736
Loading
Loading
Loading
Loading
+40 −9
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.statusbar;

import static android.app.Flags.lifetimeExtensionRefactor;

import android.annotation.NonNull;
import android.app.Notification;
import android.app.RemoteInputHistoryItem;
@@ -29,6 +31,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;

@@ -68,7 +71,7 @@ public class RemoteInputNotificationRebuilder {
    @NonNull
    public StatusBarNotification rebuildForCanceledSmartReplies(
            NotificationEntry entry) {
        return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
        return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */,
                false /* showSpinner */, null /* mimeType */, null /* uri */);
    }

@@ -97,9 +100,36 @@ public class RemoteInputNotificationRebuilder {
    StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
            CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
        StatusBarNotification sbn = entry.getSbn();

        Notification.Builder b = Notification.Builder
                .recoverBuilder(mContext, sbn.getNotification().clone());

        if (lifetimeExtensionRefactor()) {
            if (entry.remoteInputs == null) {
                entry.remoteInputs = new ArrayList<RemoteInputHistoryItem>();
            }

            // Append new remote input information to remoteInputs list
            if (remoteInputText != null || uri != null) {
                RemoteInputHistoryItem newItem = uri != null
                        ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
                        : new RemoteInputHistoryItem(remoteInputText);
                // The list is latest-first, so new elements should be added as the first element.
                entry.remoteInputs.add(0, newItem);
            }

            // Read the whole remoteInputs list from the entry, then append all of those to the sbn.
            Parcelable[] oldHistoryItems = sbn.getNotification().extras
                    .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);

            RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
                    ? Stream.concat(
                            entry.remoteInputs.stream(),
                            Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
                    .toArray(RemoteInputHistoryItem[]::new)
                    : entry.remoteInputs.toArray(RemoteInputHistoryItem[]::new);
            b.setRemoteInputHistory(newHistoryItems);

        } else {
            if (remoteInputText != null || uri != null) {
                RemoteInputHistoryItem newItem = uri != null
                        ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
@@ -114,6 +144,7 @@ public class RemoteInputNotificationRebuilder {
                        : new RemoteInputHistoryItem[]{newItem};
                b.setRemoteInputHistory(newHistoryItems);
            }
        }
        b.setShowRemoteInputSpinner(showSpinner);
        b.setHideSmartReplies(true);

+2 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager.Policy;
import android.app.Person;
import android.app.RemoteInput;
import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
@@ -127,6 +128,7 @@ public final class NotificationEntry extends ListEntry {
    public int targetSdk;
    private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
    public CharSequence remoteInputText;
    public List<RemoteInputHistoryItem> remoteInputs = null;
    public String remoteInputMimeType;
    public Uri remoteInputUri;
    public ContentInfo remoteInputAttachment;
+51 −10
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.statusbar.notification.collection.coordinator

import android.app.Flags.lifetimeExtensionRefactor
import android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
import android.os.Handler
import android.service.notification.NotificationListenerService.REASON_CANCEL
import android.service.notification.NotificationListenerService.REASON_CLICK
@@ -88,11 +90,21 @@ class RemoteInputCoordinator @Inject constructor(

    override fun attach(pipeline: NotifPipeline) {
        mNotificationRemoteInputManager.setRemoteInputListener(this)
        mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
        if (lifetimeExtensionRefactor()) {
            pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
        } else {
            mRemoteInputLifetimeExtenders.forEach {
                pipeline.addNotificationLifetimeExtender(it)
            }
        }
        mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
        pipeline.addCollectionListener(mCollectionListener)
    }

    /*
     * Listener that updates the appearance of the notification if it has been lifetime extended
     * by a a direct reply or a smart reply, and cancelled.
     */
    val mCollectionListener = object : NotifCollectionListener {
        override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
            if (DEBUG) {
@@ -100,11 +112,34 @@ class RemoteInputCoordinator @Inject constructor(
                        " fromSystem=$fromSystem)")
            }
            if (fromSystem) {
                if (lifetimeExtensionRefactor()) {
                    if ((entry.getSbn().getNotification().flags
                                    and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
                        if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
                                        entry)) {
                            val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
                            entry.onRemoteInputInserted()
                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
                                    "Extending lifetime of notification with remote input")
                        } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
                                        entry)) {
                            val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
                            mSmartReplyController.stopSending(entry)
                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
                                    "Extending lifetime of notification with smart reply")
                        }
                    } else {
                        // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
                        // should have their remote inputs list cleared.
                        entry.remoteInputs = null
                    }
                } else {
                    // Mark smart replies as sent whenever a notification is updated by the app,
                    // otherwise the smart replies are never marked as sent.
                    mSmartReplyController.stopSending(entry)
                }
            }
        }

        override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
            if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
@@ -130,8 +165,10 @@ class RemoteInputCoordinator @Inject constructor(
        // NOTE: This is some trickery! By removing the lifetime extensions when we know they should
        // be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to
        // fire again, thus ensuring that we add subsequent replies to the notification.
        if (!lifetimeExtensionRefactor()) {
            mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
            mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
        }

        // If we're extending for remote input being active, then from the apps point of
        // view it is already canceled, so we'll need to cancel it on the apps behalf
@@ -160,15 +197,19 @@ class RemoteInputCoordinator @Inject constructor(
    }

    override fun isNotificationKeptForRemoteInputHistory(key: String) =
        if (!lifetimeExtensionRefactor()) {
            mRemoteInputHistoryExtender.isExtending(key) ||
                    mSmartReplyHistoryExtender.isExtending(key)
        } else false

    override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
        if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
        if (!lifetimeExtensionRefactor()) {
            mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
                    REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
            mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
                    REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
        }
        mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
                REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
    }
+96 −5
Original line number Diff line number Diff line
@@ -15,7 +15,13 @@
 */
package com.android.systemui.statusbar.notification.collection.coordinator

import android.app.Flags.lifetimeExtensionRefactor
import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR
import android.app.Notification
import android.app.RemoteInputHistoryItem
import android.os.Handler
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -34,6 +40,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.captureMany
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -42,6 +49,7 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks

@@ -57,6 +65,7 @@ class RemoteInputCoordinatorTest : SysuiTestCase() {
    private lateinit var entry2: NotificationEntry

    @Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback

    @Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder
    @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
    @Mock private lateinit var mainHandler: Handler
@@ -84,9 +93,6 @@ class RemoteInputCoordinatorTest : SysuiTestCase() {
        listener = withArgCaptor {
            verify(remoteInputManager).setRemoteInputListener(capture())
        }
        collectionListener = withArgCaptor {
            verify(pipeline).addCollectionListener(capture())
        }
        entry1 = NotificationEntryBuilder().setId(1).build()
        entry2 = NotificationEntryBuilder().setId(2).build()
        `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
@@ -98,16 +104,23 @@ class RemoteInputCoordinatorTest : SysuiTestCase() {
    val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
    val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender

    val collectionListeners get() = captureMany {
        verify(pipeline, times(1)).addCollectionListener(capture())
    }

    @Test
    fun testRemoteInputActive() {
        `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
        assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
        if (!lifetimeExtensionRefactor()) {
            assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
            assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
        }
        assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse()
    }

    @Test
    @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
    fun testRemoteInputHistory() {
        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true)
        assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -117,6 +130,7 @@ class RemoteInputCoordinatorTest : SysuiTestCase() {
    }

    @Test
    @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
    fun testSmartReplyHistory() {
        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true)
        assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -142,4 +156,81 @@ class RemoteInputCoordinatorTest : SysuiTestCase() {
        verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1)
        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
    }

    @Test
    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
    fun testOnlyRemoteInputActiveLifetimeExtenderExtends() {
        `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
        assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue()

        listener.onPanelCollapsed()
        assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()

        // Checks that lifetimeExtensionCallback is only called the expected number of times,
        // by the remoteInputActiveExtender.
        // Checks that the remote input history extender and smart reply history extenders
        // aren't attached to the pipeline.
        verify(lifetimeExtensionCallback, times(1)).onEndLifetimeExtension(any(), any())
    }

    @Test
    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
    fun testRemoteInputLifetimeExtensionListenerTrigger() {
        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
        val entry = NotificationEntryBuilder()
                .setId(3)
                .setTag("entry")
                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
                .build()
        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true)
        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)

        collectionListeners.forEach {
            it.onEntryUpdated(entry, true)
        }

        verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
    }

    @Test
    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
    fun testSmartReplyLifetimeExtensionListenerTrigger() {
        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
        val entry = NotificationEntryBuilder()
                .setId(3)
                .setTag("entry")
                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
                .build()
        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true)
        collectionListeners.forEach {
            it.onEntryUpdated(entry, true)
        }


        verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
        verify(smartReplyController, times(1)).stopSending(entry)
    }

    @Test
    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
    fun testLifetimeExtensionListenerClearsRemoteInputs() {
        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
        val entry = NotificationEntryBuilder()
                .setId(3)
                .setTag("entry")
                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false)
                .build()
        entry.remoteInputs = ArrayList<RemoteInputHistoryItem>()
        entry.remoteInputs.add(RemoteInputHistoryItem("Test Text"))
        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)

        collectionListeners.forEach {
            it.onEntryUpdated(entry, true)
        }

        assertThat(entry.remoteInputs).isNull()
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.notification;

import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_FOREGROUND_SERVICE;
@@ -24,6 +25,7 @@ import static android.os.UserHandle.USER_ALL;
import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -1802,6 +1804,8 @@ abstract public class ManagedServices {
        public ComponentName component;
        public int userid;
        public boolean isSystem;
        @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
        public boolean isSystemUi;
        public ServiceConnection connection;
        public int targetSdkVersion;
        public Pair<ComponentName, Integer> mKey;
@@ -1836,6 +1840,11 @@ abstract public class ManagedServices {
            return isSystem;
        }

        @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
        public boolean isSystemUi() {
            return isSystemUi;
        }

        @Override
        public String toString() {
            return new StringBuilder("ManagedServiceInfo[")
Loading