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

Commit ac8feac2 authored by Alexander Roederer's avatar Alexander Roederer Committed by Android (Google) Code Review
Browse files

Merge "Send update from NMS to SysUI on LifetimeExtension" into main

parents f7d990df 5e9f078d
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