diff options
19 files changed, 346 insertions, 77 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java index c9d910c530ea..01046cd10d87 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java @@ -20,16 +20,29 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static junit.framework.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityOptions; import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.os.SystemClock; import android.os.UserHandle; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; +import android.util.Pair; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RemoteViews; -import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -42,19 +55,37 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.util.kotlin.JavaAdapter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper public class NotificationRemoteInputManagerTest extends SysuiTestCase { + + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME); + } + private static final String TEST_PACKAGE_NAME = "test"; private static final int TEST_UID = 0; @@ -69,14 +100,34 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { @Mock private NotificationClickNotifier mClickNotifier; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private PowerInteractor mPowerInteractor; + @Mock + NotificationRemoteInputManager.RemoteInputListener mRemoteInputListener; + private ActionClickLogger mActionClickLogger; + @Captor + ArgumentCaptor<NotificationRemoteInputManager.ClickHandler> mClickHandlerArgumentCaptor; + private Context mSpyContext; + private NotificationTestHelper mTestHelper; private TestableNotificationRemoteInputManager mRemoteInputManager; private NotificationEntry mEntry; + public NotificationRemoteInputManagerTest(FlagsParameterization flags) { + super(); + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before - public void setUp() { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mSpyContext = spy(mContext); + doNothing().when(mSpyContext).startIntentSender( + any(), any(), anyInt(), anyInt(), anyInt(), any()); + + + mTestHelper = new NotificationTestHelper(mSpyContext, mDependency); + mActionClickLogger = spy(new ActionClickLogger(logcatLogBuffer())); + mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext, mock(NotifPipelineFlags.class), mLockscreenUserManager, @@ -87,9 +138,10 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { mRemoteInputUriController, new RemoteInputControllerLogger(logcatLogBuffer()), mClickNotifier, - new ActionClickLogger(logcatLogBuffer()), + mActionClickLogger, mock(JavaAdapter.class), mock(ShadeInteractor.class)); + mRemoteInputManager.setRemoteInputListener(mRemoteInputListener); mEntry = new NotificationEntryBuilder() .setPkg(TEST_PACKAGE_NAME) .setOpPkg(TEST_PACKAGE_NAME) @@ -133,6 +185,70 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase { assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry)); } + @Test + public void testActionClick() throws Exception { + RemoteViews.RemoteResponse response = mock(RemoteViews.RemoteResponse.class); + when(response.getLaunchOptions(any())).thenReturn( + Pair.create(mock(Intent.class), mock(ActivityOptions.class))); + ExpandableNotificationRow row = getRowWithReplyAction(); + View actionView = ((LinearLayout) row.getPrivateLayout().getExpandedChild().findViewById( + com.android.internal.R.id.actions)).getChildAt(0); + Notification n = getNotification(row); + CountDownLatch latch = new CountDownLatch(1); + Consumer<NotificationEntry> consumer = notificationEntry -> latch.countDown(); + if (!NotificationBundleUi.isEnabled()) { + mRemoteInputManager.addActionPressListener(consumer); + } + + mRemoteInputManager.getRemoteViewsOnClickHandler().onInteraction( + actionView, + n.actions[0].actionIntent, + response); + + verify(mActionClickLogger).logInitialClick(row.getKey(), 0, n.actions[0].actionIntent); + verify(mClickNotifier).onNotificationActionClick( + eq(row.getKey()), eq(0), eq(n.actions[0]), any(), eq(false)); + verify(mCallback).handleRemoteViewClick(eq(actionView), eq(n.actions[0].actionIntent), + eq(false), eq(0), mClickHandlerArgumentCaptor.capture()); + + mClickHandlerArgumentCaptor.getValue().handleClick(); + verify(mActionClickLogger).logStartingIntentWithDefaultHandler( + row.getKey(), n.actions[0].actionIntent, 0); + + verify(mRemoteInputListener).releaseNotificationIfKeptForRemoteInputHistory(row.getKey()); + if (NotificationBundleUi.isEnabled()) { + verify(row.getEntryAdapter()).onNotificationActionClicked(); + } else { + latch.await(10, TimeUnit.MILLISECONDS); + } + } + + private Notification getNotification(ExpandableNotificationRow row) { + if (NotificationBundleUi.isEnabled()) { + return row.getEntryAdapter().getSbn().getNotification(); + } else { + return row.getEntry().getSbn().getNotification(); + } + } + + private ExpandableNotificationRow getRowWithReplyAction() throws Exception { + PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), + PendingIntent.FLAG_IMMUTABLE); + Notification n = new Notification.Builder(mSpyContext, "") + .setSmallIcon(com.android.systemui.res.R.drawable.ic_person) + .addAction(new Notification.Action(com.android.systemui.res.R.drawable.ic_person, + "reply", pi)) + .build(); + ExpandableNotificationRow row = mTestHelper.createRow(n); + row.onNotificationUpdated(); + row.getPrivateLayout().setExpandedChild(Notification.Builder.recoverBuilder(mSpyContext, n) + .createBigContentView().apply( + mSpyContext, + row.getPrivateLayout(), + mRemoteInputManager.getRemoteViewsOnClickHandler())); + return row; + } + private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager { TestableNotificationRemoteInputManager( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt index b6889afa4e8a..faafa073be4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt @@ -29,8 +29,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.mockNotificationActivityStarter import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.entryAdapterFactory +import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.testKosmos @@ -355,16 +357,27 @@ class NotificationEntryAdapterTest : SysuiTestCase() { val notification: Notification = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() - val entry = - NotificationEntryBuilder() - .setNotification(notification) - .setImportance(NotificationManager.IMPORTANCE_MIN) - .build() + val entry = NotificationEntryBuilder().setNotification(notification).build() underTest = factory.create(entry) as NotificationEntryAdapter underTest.onNotificationBubbleIconClicked() - verify((factory as? EntryAdapterFactoryImpl)?.getNotificationActivityStarter()) - ?.onNotificationBubbleIconClicked(entry) + verify(kosmos.mockNotificationActivityStarter).onNotificationBubbleIconClicked(entry) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun onNotificationActionClicked() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .addAction(Mockito.mock(Notification.Action::class.java)) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + underTest.onNotificationActionClicked() + verify(kosmos.mockNotificationActionClickManager).onNotificationActionClicked(entry) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 30983550f0f9..44d88c31c5f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -50,6 +50,8 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider +import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor @@ -138,6 +140,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { headsUpViewBinder, visualInterruptionDecisionProvider, remoteInputManager, + kosmos.mockNotificationActionClickManager, launchFullScreenIntentProvider, flags, statusBarNotificationChipsInteractor, @@ -161,8 +164,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(notifPipeline).addOnBeforeFinalizeFilterListener(capture()) } onHeadsUpChangedListener = withArgCaptor { verify(headsUpManager).addListener(capture()) } - actionPressListener = withArgCaptor { - verify(remoteInputManager).addActionPressListener(capture()) + actionPressListener = if (NotificationBundleUi.isEnabled) { + withArgCaptor { + verify(kosmos.mockNotificationActionClickManager).addActionClickListener(capture()) + } + } else { + withArgCaptor { + verify(remoteInputManager).addActionPressListener(capture()) + } } given(headsUpManager.allEntries).willAnswer { huns.stream() } given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation -> @@ -260,7 +269,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { addHUN(entry) actionPressListener.accept(entry) - verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry) + verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry.key) whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(true) assertFalse(notifLifetimeExtender.maybeExtendLifetime(entry, 0)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 9804932918dc..8560b66d961f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -1071,7 +1071,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse() - underTest.setUserActionMayIndirectlyRemove(notifEntry) + underTest.setUserActionMayIndirectlyRemove(notifEntry.key) assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 6415f8c25a37..784743740434 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -743,11 +744,12 @@ public class NotificationTestHelper { mock(MetricsLogger.class), mock(PeopleNotificationIdentifier.class), mock(NotificationIconStyleProvider.class), - mock(VisualStabilityCoordinator.class) + mock(VisualStabilityCoordinator.class), + mock(NotificationActionClickManager.class) ).create(entry); row.initialize( - entryAdapter, + spy(entryAdapter), entry, mock(RemoteInputViewSubcomponent.Factory.class), APP_NAME, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 0d34bdc7e477..041ed6504634 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -59,11 +59,13 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule; import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.RemoteInputControllerLogger; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.policy.RemoteInputView; @@ -134,20 +136,21 @@ public class NotificationRemoteInputManager implements CoreStartable { view.getTag(com.android.internal.R.id.notification_action_index_tag); final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent()); - final NotificationEntry entry = getNotificationForParent(view.getParent()); - mLogger.logInitialClick( - row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent); + if (row == null) { + return false; + } + mLogger.logInitialClick(row.getLoggingKey(), actionIndex, pendingIntent); if (handleRemoteInput(view, pendingIntent)) { - mLogger.logRemoteInputWasHandled( - row != null ? row.getLoggingKey() : null, actionIndex); + mLogger.logRemoteInputWasHandled(row.getLoggingKey(), actionIndex); return true; } if (DEBUG) { Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); } - logActionClick(view, entry, pendingIntent); + Notification.Action action = getActionFromView(view, row, pendingIntent); + logActionClick(view, row.getKey(), action); // The intent we are sending is for the application, which // won't have permission to immediately start an activity after // the user switches to home. We know it is safe to do at this @@ -156,33 +159,47 @@ public class NotificationRemoteInputManager implements CoreStartable { ActivityManager.getService().resumeAppSwitches(); } catch (RemoteException e) { } - Notification.Action action = getActionFromView(view, entry, pendingIntent); return mCallback.handleRemoteViewClick(view, pendingIntent, action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> { Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); mLogger.logStartingIntentWithDefaultHandler( - row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex); + row.getLoggingKey(), pendingIntent, actionIndex); boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options); - if (started) releaseNotificationIfKeptForRemoteInputHistory(entry); + if (started) { + if (NotificationBundleUi.isEnabled()) { + releaseNotificationIfKeptForRemoteInputHistory(row.getEntryAdapter()); + } else { + releaseNotificationIfKeptForRemoteInputHistory(row.getEntry()); + } + } return started; }); } private @Nullable Notification.Action getActionFromView(View view, - NotificationEntry entry, PendingIntent actionIntent) { + ExpandableNotificationRow row, PendingIntent actionIntent) { Integer actionIndex = (Integer) view.getTag(com.android.internal.R.id.notification_action_index_tag); if (actionIndex == null) { return null; } - if (entry == null) { + StatusBarNotification statusBarNotification = null; + if (NotificationBundleUi.isEnabled()) { + if (row.getEntryAdapter() != null) { + statusBarNotification = row.getEntryAdapter().getSbn(); + } + } else { + if (row.getEntry() != null) { + statusBarNotification = row.getEntry().getSbn(); + } + } + if (statusBarNotification == null) { Log.w(TAG, "Couldn't determine notification for click."); return null; } // Notification may be updated before this function is executed, and thus play safe // here and verify that the action object is still the one that where the click happens. - StatusBarNotification statusBarNotification = entry.getSbn(); Notification.Action[] actions = statusBarNotification.getNotification().actions; if (actions == null || actionIndex >= actions.length) { Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid"); @@ -199,14 +216,12 @@ public class NotificationRemoteInputManager implements CoreStartable { private void logActionClick( View view, - NotificationEntry entry, - PendingIntent actionIntent) { - Notification.Action action = getActionFromView(view, entry, actionIntent); + String key, + Notification.Action action) { if (action == null) { return; } ViewParent parent = view.getParent(); - String key = entry.getSbn().getKey(); int buttonIndex = -1; // If this is a default template, determine the index of the button. if (view.getId() == com.android.internal.R.id.action0 && @@ -214,20 +229,10 @@ public class NotificationRemoteInputManager implements CoreStartable { ViewGroup actionGroup = (ViewGroup) parent; buttonIndex = actionGroup.indexOfChild(view); } - final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); + final NotificationVisibility nv = mVisibilityProvider.obtain(key, true); mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false); } - private NotificationEntry getNotificationForParent(ViewParent parent) { - while (parent != null) { - if (parent instanceof ExpandableNotificationRow) { - return ((ExpandableNotificationRow) parent).getEntry(); - } - parent = parent.getParent(); - } - return null; - } - private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) { while (parent != null) { if (parent instanceof ExpandableNotificationRow) { @@ -394,11 +399,21 @@ public class NotificationRemoteInputManager implements CoreStartable { } } + /** + * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager} + * instead + */ public void addActionPressListener(Consumer<NotificationEntry> listener) { + NotificationBundleUi.assertInLegacyMode(); mActionPressListeners.addIfAbsent(listener); } + /** + * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager} + * instead + */ public void removeActionPressListener(Consumer<NotificationEntry> listener) { + NotificationBundleUi.assertInLegacyMode(); mActionPressListeners.remove(listener); } @@ -681,12 +696,30 @@ public class NotificationRemoteInputManager implements CoreStartable { * (after unlock, if applicable), and will then wait a short time to allow the app to update the * notification in response to the action. */ + private void releaseNotificationIfKeptForRemoteInputHistory(EntryAdapter entryAdapter) { + if (entryAdapter == null) { + return; + } + if (mRemoteInputListener != null) { + mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory( + entryAdapter.getKey()); + } + entryAdapter.onNotificationActionClicked(); + } + + /** + * Checks if the notification is being kept due to the user sending an inline reply, and if + * so, releases that hold. This is called anytime an action on the notification is dispatched + * (after unlock, if applicable), and will then wait a short time to allow the app to update the + * notification in response to the action. + */ private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry == null) { return; } if (mRemoteInputListener != null) { - mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry); + mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry.getKey()); } for (Consumer<NotificationEntry> listener : mActionPressListeners) { listener.accept(entry); @@ -866,7 +899,7 @@ public class NotificationRemoteInputManager implements CoreStartable { boolean isNotificationKeptForRemoteInputHistory(@NonNull String key); /** Called on user interaction to end lifetime extension for history */ - void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry); + void releaseNotificationIfKeptForRemoteInputHistory(@NonNull String entryKey); /** Called when the RemoteInputController is attached to the manager */ void setRemoteInputController(@NonNull RemoteInputController remoteInputController); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt index 64db9df8270c..26c302bf6409 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt @@ -20,6 +20,7 @@ import android.app.Notification import android.content.Context import android.os.Build import android.service.notification.StatusBarNotification +import android.util.Log import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import kotlinx.coroutines.flow.StateFlow @@ -118,5 +119,13 @@ class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter { override fun onNotificationBubbleIconClicked() { // do nothing. these cannot be a bubble + Log.wtf(TAG, "onNotificationBubbleIconClicked() called") + } + + override fun onNotificationActionClicked() { + // do nothing. these have no actions + Log.wtf(TAG, "onNotificationActionClicked() called") } } + +private const val TAG = "BundleEntryAdapter" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 0e75b6050678..3118ce56ac69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -140,5 +140,10 @@ public interface EntryAdapter { * Process a click on a notification bubble icon */ void onNotificationBubbleIconClicked(); + + /** + * Processes a click on a notification action + */ + void onNotificationActionClicked(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt index 779c25a3b402..a5169865c3c9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt @@ -16,11 +16,11 @@ package com.android.systemui.statusbar.notification.collection -import androidx.annotation.VisibleForTesting import com.android.internal.logging.MetricsLogger import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import javax.inject.Inject @@ -33,6 +33,7 @@ constructor( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, private val iconStyleProvider: NotificationIconStyleProvider, private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val notificationActionClickManager: NotificationActionClickManager, ) : EntryAdapterFactory { override fun create(entry: PipelineEntry): EntryAdapter { return if (entry is NotificationEntry) { @@ -42,15 +43,11 @@ constructor( peopleNotificationIdentifier, iconStyleProvider, visualStabilityCoordinator, + notificationActionClickManager, entry, ) } else { BundleEntryAdapter((entry as BundleEntry)) } } - - @VisibleForTesting - fun getNotificationActivityStarter() : NotificationActivityStarter { - return notificationActivityStarter - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt index 0ff2dd7c7f43..1168c647c26a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.Visual import com.android.systemui.statusbar.notification.icon.IconPack import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import kotlinx.coroutines.flow.StateFlow @@ -33,6 +34,7 @@ class NotificationEntryAdapter( private val peopleNotificationIdentifier: PeopleNotificationIdentifier, private val iconStyleProvider: NotificationIconStyleProvider, private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val notificationActionClickManager: NotificationActionClickManager, private val entry: NotificationEntry, ) : EntryAdapter { @@ -142,4 +144,8 @@ class NotificationEntryAdapter( override fun onNotificationBubbleIconClicked() { notificationActivityStarter.onNotificationBubbleIconClicked(entry) } + + override fun onNotificationActionClicked() { + notificationActionClickManager.onNotificationActionClicked(entry) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 5d981b34edd3..a0eab43f854b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -48,7 +48,9 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock @@ -83,6 +85,7 @@ constructor( private val mHeadsUpViewBinder: HeadsUpViewBinder, private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider, private val mRemoteInputManager: NotificationRemoteInputManager, + private val notificationActionClickManager: NotificationActionClickManager, private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, private val mFlags: NotifPipelineFlags, private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor, @@ -108,7 +111,11 @@ constructor( pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter) pipeline.addPromoter(mNotifPromoter) pipeline.addNotificationLifetimeExtender(mLifetimeExtender) - mRemoteInputManager.addActionPressListener(mActionPressListener) + if (NotificationBundleUi.isEnabled) { + notificationActionClickManager.addActionClickListener(mActionPressListener) + } else { + mRemoteInputManager.addActionPressListener(mActionPressListener) + } if (StatusBarNotifChips.isEnabled) { applicationScope.launch { @@ -783,7 +790,7 @@ constructor( */ private val mActionPressListener = Consumer<NotificationEntry> { entry -> - mHeadsUpManager.setUserActionMayIndirectlyRemove(entry) + mHeadsUpManager.setUserActionMayIndirectlyRemove(entry.key) mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt index e7c767f42c12..27c0dcccfe43 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt @@ -221,20 +221,20 @@ constructor( mSmartReplyHistoryExtender.isExtending(key) } else false - override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) { - if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})") + override fun releaseNotificationIfKeptForRemoteInputHistory(entryKey: String) { + if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entryKey})") if (!lifetimeExtensionRefactor()) { mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) } mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay( - entry.key, + entryKey, REMOTE_INPUT_EXTENDER_RELEASE_DELAY, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index ef3da9498f70..1e5aa01714be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -83,6 +83,7 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; +import com.android.systemui.statusbar.notification.row.NotificationActionClickManager; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -346,4 +347,5 @@ public interface NotificationsModule { /** Provides an instance of {@link EntryAdapterFactory} */ @Binds EntryAdapterFactory provideEntryAdapterFactory(EntryAdapterFactoryImpl impl); + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt index 9728fcfcd6ba..25ae50c34659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.headsup import android.graphics.Region import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import dagger.Binds @@ -155,9 +154,9 @@ interface HeadsUpManager : Dumpable { fun setAnimationStateHandler(handler: AnimationStateHandler) /** - * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until - * it's collapsed again. - */ + * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until + * it's collapsed again. + */ fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean) /** @@ -199,12 +198,12 @@ interface HeadsUpManager : Dumpable { * Notes that the user took an action on an entry that might indirectly cause the system or the * app to remove the notification. * - * @param entry the entry that might be indirectly removed by the user's action + * @param entry the key of the entry that might be indirectly removed by the user's action * @see * com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator.mActionPressListener * @see .canRemoveImmediately */ - fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) + fun setUserActionMayIndirectlyRemove(entryKey: String) /** * Decides whether a click is invalid for a notification, i.e. it has not been shown long enough @@ -332,7 +331,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager { override fun setUser(user: Int) {} - override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {} + override fun setUserActionMayIndirectlyRemove(entryKey: String) {} override fun shouldSwallowClick(key: String): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index ca94655318b9..ca83666079ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -1126,8 +1126,8 @@ public class HeadsUpManagerImpl * @see HeadsUpCoordinator.mActionPressListener * @see #canRemoveImmediately(String) */ - public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) { - HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); + public void setUserActionMayIndirectlyRemove(@NonNull String entryKey) { + HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey); if (headsUpEntry != null) { headsUpEntry.mUserActionMayIndirectlyRemove = true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt new file mode 100644 index 000000000000..2b451406eaad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.util.ListenerSet +import java.util.function.Consumer +import javax.inject.Inject + +/** + * Pipeline components can register consumers here to be informed when a notification action is + * clicked + */ +@SysUISingleton +class NotificationActionClickManager @Inject constructor() { + private val actionClickListeners = ListenerSet<Consumer<NotificationEntry>>() + + fun addActionClickListener(listener: Consumer<NotificationEntry>) { + actionClickListeners.addIfAbsent(listener) + } + + fun removeActionClickListener(listener: Consumer<NotificationEntry>) { + actionClickListeners.remove(listener) + } + + fun onNotificationActionClicked(entry: NotificationEntry) { + for (listener in actionClickListeners) { + listener.accept(entry) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt index e99f61e7cd13..067e420b89c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt @@ -25,12 +25,13 @@ import com.android.systemui.statusbar.notification.people.peopleNotificationIden import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider val Kosmos.entryAdapterFactory by -Kosmos.Fixture { - EntryAdapterFactoryImpl( - mockNotificationActivityStarter, - metricsLogger, - peopleNotificationIdentifier, - notificationIconStyleProvider, - visualStabilityCoordinator, - ) -}
\ No newline at end of file + Kosmos.Fixture { + EntryAdapterFactoryImpl( + mockNotificationActivityStarter, + metricsLogger, + peopleNotificationIdentifier, + notificationIconStyleProvider, + visualStabilityCoordinator, + mockNotificationActionClickManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index 7f012ba25ab9..6a674ca29ca4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -375,6 +375,7 @@ class ExpandableNotificationRowBuilder( Mockito.mock(PeopleNotificationIdentifier::class.java), Mockito.mock(NotificationIconStyleProvider::class.java), Mockito.mock(VisualStabilityCoordinator::class.java), + Mockito.mock(NotificationActionClickManager::class.java), ) .create(entry) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt new file mode 100644 index 000000000000..8e62ae8825f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +var Kosmos.mockNotificationActionClickManager: NotificationActionClickManager by + Kosmos.Fixture { mock<NotificationActionClickManager>() } |