From f3ab1c4a6d6a3e757032667b075997c938a18c47 Mon Sep 17 00:00:00 2001 From: Jeff DeCew Date: Fri, 14 Jan 2022 21:03:36 +0000 Subject: [PATCH] Convert HeadsUpCoordinator/Test to Kotlin Bug: 213368402 Test: atest HeadsUpCoordinatorTest Change-Id: If86cbcdaba23d330e2db42b4fc31f22d139e11ec --- .../coordinator/HeadsUpCoordinator.java | 259 ----------------- .../coordinator/HeadsUpCoordinator.kt | 200 +++++++++++++ .../coordinator/HeadsUpCoordinatorTest.java | 274 ------------------ .../coordinator/HeadsUpCoordinatorTest.kt | 238 +++++++++++++++ 4 files changed, 438 insertions(+), 533 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt delete mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java deleted file mode 100644 index 74109120149ed..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (C) 2020 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.collection.coordinator; - -import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY; -import static com.android.systemui.statusbar.notification.interruption.HeadsUpController.alertAgain; - -import android.util.ArraySet; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.notification.collection.ListEntry; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; -import com.android.systemui.statusbar.notification.collection.render.NodeController; -import com.android.systemui.statusbar.notification.dagger.IncomingHeader; -import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder; -import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; -import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; -import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import javax.inject.Inject; - -/** - * Coordinates heads up notification (HUN) interactions with the notification pipeline based on - * the HUN state reported by the {@link HeadsUpManager}. In this class we only consider one - * notification, in particular the {@link HeadsUpManager#getTopEntry()}, to be HeadsUpping at a - * time even though other notifications may be queued to heads up next. - * - * The current HUN, but not HUNs that are queued to heads up, will be: - * - Lifetime extended until it's no longer heads upping. - * - Promoted out of its group if it's a child of a group. - * - In the HeadsUpCoordinatorSection. Ordering is configured in {@link NotifCoordinators}. - * - Removed from HeadsUpManager if it's removed from the NotificationCollection. - * - * Note: The inflation callback in {@link PreparationCoordinator} handles showing HUNs. - */ -@CoordinatorScope -public class HeadsUpCoordinator implements Coordinator { - private static final String TAG = "HeadsUpCoordinator"; - - private final HeadsUpManager mHeadsUpManager; - private final HeadsUpViewBinder mHeadsUpViewBinder; - private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; - private final NotificationRemoteInputManager mRemoteInputManager; - private final NodeController mIncomingHeaderController; - private final DelayableExecutor mExecutor; - - private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; - // notifs we've extended the lifetime for - private final ArraySet mNotifsExtendingLifetime = new ArraySet<>(); - - @Inject - public HeadsUpCoordinator( - HeadsUpManager headsUpManager, - HeadsUpViewBinder headsUpViewBinder, - NotificationInterruptStateProvider notificationInterruptStateProvider, - NotificationRemoteInputManager remoteInputManager, - @IncomingHeader NodeController incomingHeaderController, - @Main DelayableExecutor executor) { - mHeadsUpManager = headsUpManager; - mHeadsUpViewBinder = headsUpViewBinder; - mNotificationInterruptStateProvider = notificationInterruptStateProvider; - mRemoteInputManager = remoteInputManager; - mIncomingHeaderController = incomingHeaderController; - mExecutor = executor; - } - - @Override - public void attach(NotifPipeline pipeline) { - mHeadsUpManager.addListener(mOnHeadsUpChangedListener); - pipeline.addCollectionListener(mNotifCollectionListener); - pipeline.addPromoter(mNotifPromoter); - pipeline.addNotificationLifetimeExtender(mLifetimeExtender); - } - - public NotifSectioner getSectioner() { - return mNotifSectioner; - } - - private void onHeadsUpViewBound(NotificationEntry entry) { - mHeadsUpManager.showNotification(entry); - } - - private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() { - /** - * Notification was just added and if it should heads up, bind the view and then show it. - */ - @Override - public void onEntryAdded(NotificationEntry entry) { - if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) { - mHeadsUpViewBinder.bindHeadsUpView( - entry, - HeadsUpCoordinator.this::onHeadsUpViewBound); - } - } - - /** - * Notification could've updated to be heads up or not heads up. Even if it did update to - * heads up, if the notification specified that it only wants to alert once, don't heads - * up again. - */ - @Override - public void onEntryUpdated(NotificationEntry entry) { - boolean hunAgain = alertAgain(entry, entry.getSbn().getNotification()); - // includes check for whether this notification should be filtered: - boolean shouldHeadsUp = mNotificationInterruptStateProvider.shouldHeadsUp(entry); - final boolean wasHeadsUp = mHeadsUpManager.isAlerting(entry.getKey()); - if (wasHeadsUp) { - if (shouldHeadsUp) { - mHeadsUpManager.updateNotification(entry.getKey(), hunAgain); - } else if (!mHeadsUpManager.isEntryAutoHeadsUpped(entry.getKey())) { - // We don't want this to be interrupting anymore, let's remove it - mHeadsUpManager.removeNotification( - entry.getKey(), false /* removeImmediately */); - } - } else if (shouldHeadsUp && hunAgain) { - // This notification was updated to be heads up, show it! - mHeadsUpViewBinder.bindHeadsUpView( - entry, - HeadsUpCoordinator.this::onHeadsUpViewBound); - } - } - - /** - * Stop alerting HUNs that are removed from the notification collection - */ - @Override - public void onEntryRemoved(NotificationEntry entry, int reason) { - final String entryKey = entry.getKey(); - if (mHeadsUpManager.isAlerting(entryKey)) { - boolean removeImmediatelyForRemoteInput = - mRemoteInputManager.isSpinning(entryKey) - && !FORCE_REMOTE_INPUT_HISTORY; - mHeadsUpManager.removeNotification(entry.getKey(), removeImmediatelyForRemoteInput); - } - } - - @Override - public void onEntryCleanUp(NotificationEntry entry) { - mHeadsUpViewBinder.abortBindCallback(entry); - } - }; - - private final NotifLifetimeExtender mLifetimeExtender = new NotifLifetimeExtender() { - @Override - public @NonNull String getName() { - return TAG; - } - - @Override - public void setCallback(@NonNull OnEndLifetimeExtensionCallback callback) { - mEndLifetimeExtension = callback; - } - - @Override - public boolean maybeExtendLifetime(@NonNull NotificationEntry entry, int reason) { - boolean extend = !mHeadsUpManager.canRemoveImmediately(entry.getKey()); - if (extend) { - if (isSticky(entry)) { - long removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.getKey()); - mExecutor.executeDelayed(() -> { - if (mNotifsExtendingLifetime.contains(entry) - && mHeadsUpManager.canRemoveImmediately(entry.getKey())) { - mHeadsUpManager.removeNotification( - entry.getKey(), /* releaseImmediately */ true); - } - }, removeAfterMillis); - } else { - // remove as early as possible - mExecutor.execute( - () -> mHeadsUpManager.removeNotification( - entry.getKey(), /* releaseImmediately */ false)); - } - mNotifsExtendingLifetime.add(entry); - } - return extend; - } - - @Override - public void cancelLifetimeExtension(@NonNull NotificationEntry entry) { - mNotifsExtendingLifetime.remove(entry); - } - }; - - private final NotifPromoter mNotifPromoter = new NotifPromoter(TAG) { - @Override - public boolean shouldPromoteToTopLevel(NotificationEntry entry) { - return isCurrentlyShowingHun(entry); - } - }; - - private final NotifSectioner mNotifSectioner = new NotifSectioner("HeadsUp", - NotificationPriorityBucketKt.BUCKET_HEADS_UP) { - @Override - public boolean isInSection(ListEntry entry) { - return isCurrentlyShowingHun(entry); - } - - @Nullable - @Override - public NodeController getHeaderNodeController() { - // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController - if (RankingCoordinator.SHOW_ALL_SECTIONS) { - return mIncomingHeaderController; - } - return null; - } - }; - - private final OnHeadsUpChangedListener mOnHeadsUpChangedListener = - new OnHeadsUpChangedListener() { - @Override - public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { - if (!isHeadsUp) { - mHeadsUpViewBinder.unbindHeadsUpView(entry); - endNotifLifetimeExtensionIfExtended(entry); - } - } - }; - - private boolean isSticky(NotificationEntry entry) { - return mHeadsUpManager.isSticky(entry.getKey()); - } - - private boolean isCurrentlyShowingHun(ListEntry entry) { - return mHeadsUpManager.isAlerting(entry.getKey()); - } - - private void endNotifLifetimeExtensionIfExtended(NotificationEntry entry) { - if (mNotifsExtendingLifetime.remove(entry)) { - mEndLifetimeExtension.onEndLifetimeExtension(mLifetimeExtender, 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 new file mode 100644 index 0000000000000..b84b382330737 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2020 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.collection.coordinator + +import android.util.ArraySet +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback +import com.android.systemui.statusbar.notification.collection.render.NodeController +import com.android.systemui.statusbar.notification.dagger.IncomingHeader +import com.android.systemui.statusbar.notification.interruption.HeadsUpController +import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider +import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import com.android.systemui.util.concurrency.DelayableExecutor +import javax.inject.Inject + +/** + * Coordinates heads up notification (HUN) interactions with the notification pipeline based on + * the HUN state reported by the [HeadsUpManager]. In this class we only consider one + * notification, in particular the [HeadsUpManager.getTopEntry], to be HeadsUpping at a + * time even though other notifications may be queued to heads up next. + * + * The current HUN, but not HUNs that are queued to heads up, will be: + * - Lifetime extended until it's no longer heads upping. + * - Promoted out of its group if it's a child of a group. + * - In the HeadsUpCoordinatorSection. Ordering is configured in [NotifCoordinators]. + * - Removed from HeadsUpManager if it's removed from the NotificationCollection. + * + * Note: The inflation callback in [PreparationCoordinator] handles showing HUNs. + */ +@CoordinatorScope +class HeadsUpCoordinator @Inject constructor( + private val mHeadsUpManager: HeadsUpManager, + private val mHeadsUpViewBinder: HeadsUpViewBinder, + private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider, + private val mRemoteInputManager: NotificationRemoteInputManager, + @IncomingHeader private val mIncomingHeaderController: NodeController, + @Main private val mExecutor: DelayableExecutor +) : Coordinator { + private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null + + // notifs we've extended the lifetime for + private val mNotifsExtendingLifetime = ArraySet() + + override fun attach(pipeline: NotifPipeline) { + mHeadsUpManager.addListener(mOnHeadsUpChangedListener) + pipeline.addCollectionListener(mNotifCollectionListener) + pipeline.addPromoter(mNotifPromoter) + pipeline.addNotificationLifetimeExtender(mLifetimeExtender) + } + + private fun onHeadsUpViewBound(entry: NotificationEntry) { + mHeadsUpManager.showNotification(entry) + } + + private val mNotifCollectionListener = object : NotifCollectionListener { + /** + * Notification was just added and if it should heads up, bind the view and then show it. + */ + override fun onEntryAdded(entry: NotificationEntry) { + if (mNotificationInterruptStateProvider.shouldHeadsUp(entry)) { + mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) } + } + } + + /** + * Notification could've updated to be heads up or not heads up. Even if it did update to + * heads up, if the notification specified that it only wants to alert once, don't heads + * up again. + */ + override fun onEntryUpdated(entry: NotificationEntry) { + val hunAgain = HeadsUpController.alertAgain(entry, entry.sbn.notification) + // includes check for whether this notification should be filtered: + val shouldHeadsUp = mNotificationInterruptStateProvider.shouldHeadsUp(entry) + val wasHeadsUp = mHeadsUpManager.isAlerting(entry.key) + if (wasHeadsUp) { + if (shouldHeadsUp) { + mHeadsUpManager.updateNotification(entry.key, hunAgain) + } else if (!mHeadsUpManager.isEntryAutoHeadsUpped(entry.key)) { + // We don't want this to be interrupting anymore, let's remove it + mHeadsUpManager.removeNotification( + entry.key, false /* removeImmediately */ + ) + } + } else if (shouldHeadsUp && hunAgain) { + // This notification was updated to be heads up, show it! + mHeadsUpViewBinder.bindHeadsUpView(entry) { entry -> onHeadsUpViewBound(entry) } + } + } + + /** + * Stop alerting HUNs that are removed from the notification collection + */ + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + val entryKey = entry.key + if (mHeadsUpManager.isAlerting(entryKey)) { + val removeImmediatelyForRemoteInput = (mRemoteInputManager.isSpinning(entryKey) && + !NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY) + mHeadsUpManager.removeNotification(entry.key, removeImmediatelyForRemoteInput) + } + } + + override fun onEntryCleanUp(entry: NotificationEntry) { + mHeadsUpViewBinder.abortBindCallback(entry) + } + } + + private val mLifetimeExtender = object : NotifLifetimeExtender { + override fun getName() = TAG + + override fun setCallback(callback: OnEndLifetimeExtensionCallback) { + mEndLifetimeExtension = callback + } + + override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean { + if (mHeadsUpManager.canRemoveImmediately(entry.key)) { + return false + } + if (isSticky(entry)) { + val removeAfterMillis = mHeadsUpManager.getEarliestRemovalTime(entry.key) + mExecutor.executeDelayed({ + val canStillRemove = mHeadsUpManager.canRemoveImmediately(entry.key) + if (mNotifsExtendingLifetime.contains(entry) && canStillRemove) { + mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ true) + } + }, removeAfterMillis) + } else { + mExecutor.execute { + mHeadsUpManager.removeNotification(entry.key, /* releaseImmediately */ false) + } + } + mNotifsExtendingLifetime.add(entry) + return true + } + + override fun cancelLifetimeExtension(entry: NotificationEntry) { + mNotifsExtendingLifetime.remove(entry) + } + } + + private val mNotifPromoter = object : NotifPromoter(TAG) { + override fun shouldPromoteToTopLevel(entry: NotificationEntry): Boolean = + isCurrentlyShowingHun(entry) + } + + val sectioner = object : NotifSectioner("HeadsUp", BUCKET_HEADS_UP) { + override fun isInSection(entry: ListEntry): Boolean = isCurrentlyShowingHun(entry) + + override fun getHeaderNodeController(): NodeController? = + // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mIncomingHeaderController + if (RankingCoordinator.SHOW_ALL_SECTIONS) mIncomingHeaderController else null + } + + private val mOnHeadsUpChangedListener = object : OnHeadsUpChangedListener { + override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { + if (!isHeadsUp) { + mHeadsUpViewBinder.unbindHeadsUpView(entry) + endNotifLifetimeExtensionIfExtended(entry) + } + } + } + + private fun isSticky(entry: NotificationEntry) = mHeadsUpManager.isSticky(entry.key) + + private fun isCurrentlyShowingHun(entry: ListEntry) = mHeadsUpManager.isAlerting(entry.key) + + private fun endNotifLifetimeExtensionIfExtended(entry: NotificationEntry) { + if (mNotifsExtendingLifetime.remove(entry)) { + mEndLifetimeExtension?.onEndLifetimeExtension(mLifetimeExtender, entry) + } + } + + companion object { + private const val TAG = "HeadsUpCoordinator" + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java deleted file mode 100644 index 8ee892c4be581..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2020 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.collection.coordinator; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; -import com.android.systemui.statusbar.notification.collection.render.NodeController; -import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder; -import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; -import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback; -import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class HeadsUpCoordinatorTest extends SysuiTestCase { - - private HeadsUpCoordinator mCoordinator; - - // captured listeners and pluggables: - private NotifCollectionListener mCollectionListener; - private NotifPromoter mNotifPromoter; - private NotifLifetimeExtender mNotifLifetimeExtender; - private OnHeadsUpChangedListener mOnHeadsUpChangedListener; - private NotifSectioner mNotifSectioner; - - @Mock private NotifPipeline mNotifPipeline; - @Mock private HeadsUpManager mHeadsUpManager; - @Mock private HeadsUpViewBinder mHeadsUpViewBinder; - @Mock private NotificationInterruptStateProvider mNotificationInterruptStateProvider; - @Mock private NotificationRemoteInputManager mRemoteInputManager; - @Mock private NotifLifetimeExtender.OnEndLifetimeExtensionCallback mEndLifetimeExtension; - @Mock private NodeController mHeaderController; - - private NotificationEntry mEntry; - private final FakeSystemClock mClock = new FakeSystemClock(); - private final FakeExecutor mExecutor = new FakeExecutor(mClock); - private final ArrayList mHuns = new ArrayList(); - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mCoordinator = new HeadsUpCoordinator( - mHeadsUpManager, - mHeadsUpViewBinder, - mNotificationInterruptStateProvider, - mRemoteInputManager, - mHeaderController, - mExecutor); - - mCoordinator.attach(mNotifPipeline); - - // capture arguments: - ArgumentCaptor notifCollectionCaptor = - ArgumentCaptor.forClass(NotifCollectionListener.class); - ArgumentCaptor notifPromoterCaptor = - ArgumentCaptor.forClass(NotifPromoter.class); - ArgumentCaptor notifLifetimeExtenderCaptor = - ArgumentCaptor.forClass(NotifLifetimeExtender.class); - ArgumentCaptor headsUpChangedListenerCaptor = - ArgumentCaptor.forClass(OnHeadsUpChangedListener.class); - - verify(mNotifPipeline).addCollectionListener(notifCollectionCaptor.capture()); - verify(mNotifPipeline).addPromoter(notifPromoterCaptor.capture()); - verify(mNotifPipeline).addNotificationLifetimeExtender( - notifLifetimeExtenderCaptor.capture()); - verify(mHeadsUpManager).addListener(headsUpChangedListenerCaptor.capture()); - - given(mHeadsUpManager.getAllEntries()).willAnswer(i -> mHuns.stream()); - given(mHeadsUpManager.isAlerting(anyString())).willAnswer(i -> { - String key = i.getArgument(0); - for (NotificationEntry entry : mHuns) { - if (entry.getKey().equals(key)) return true; - } - return false; - }); - when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L); - - mCollectionListener = notifCollectionCaptor.getValue(); - mNotifPromoter = notifPromoterCaptor.getValue(); - mNotifLifetimeExtender = notifLifetimeExtenderCaptor.getValue(); - mOnHeadsUpChangedListener = headsUpChangedListenerCaptor.getValue(); - - mNotifSectioner = mCoordinator.getSectioner(); - mNotifLifetimeExtender.setCallback(mEndLifetimeExtension); - mEntry = new NotificationEntryBuilder().build(); - } - - @Test - public void testCancelStickyNotification() { - when(mHeadsUpManager.isSticky(anyString())).thenReturn(true); - addHUN(mEntry); - when(mHeadsUpManager.canRemoveImmediately(anyString())).thenReturn(false, true); - when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 0L); - assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0)); - mClock.advanceTime(1000L); - mExecutor.runAllReady(); - verify(mHeadsUpManager, times(0)) - .removeNotification(anyString(), eq(false)); - verify(mHeadsUpManager, times(1)) - .removeNotification(anyString(), eq(true)); - } - - @Test - public void testCancelUpdatedStickyNotification() { - when(mHeadsUpManager.isSticky(anyString())).thenReturn(true); - addHUN(mEntry); - when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L); - assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0)); - mClock.advanceTime(1000L); - mExecutor.runAllReady(); - verify(mHeadsUpManager, times(0)) - .removeNotification(anyString(), eq(false)); - verify(mHeadsUpManager, times(0)) - .removeNotification(anyString(), eq(true)); - } - - @Test - public void testCancelNotification() { - when(mHeadsUpManager.isSticky(anyString())).thenReturn(false); - addHUN(mEntry); - when(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L); - assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0)); - mClock.advanceTime(1000L); - mExecutor.runAllReady(); - verify(mHeadsUpManager, times(1)) - .removeNotification(anyString(), eq(false)); - verify(mHeadsUpManager, times(0)) - .removeNotification(anyString(), eq(true)); - } - - @Test - public void testPromotesCurrentHUN() { - // GIVEN the current HUN is set to mEntry - addHUN(mEntry); - - // THEN only promote the current HUN, mEntry - assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)); - assertFalse(mNotifPromoter.shouldPromoteToTopLevel(new NotificationEntryBuilder() - .setPkg("test-package2") - .build())); - } - - @Test - public void testIncludeInSectionCurrentHUN() { - // GIVEN the current HUN is set to mEntry - addHUN(mEntry); - - // THEN only section the current HUN, mEntry - assertTrue(mNotifSectioner.isInSection(mEntry)); - assertFalse(mNotifSectioner.isInSection(new NotificationEntryBuilder() - .setPkg("test-package") - .build())); - } - - @Test - public void testLifetimeExtendsCurrentHUN() { - // GIVEN there is a HUN, mEntry - addHUN(mEntry); - - given(mHeadsUpManager.canRemoveImmediately(anyString())).willAnswer(i -> { - String key = i.getArgument(0); - for (NotificationEntry entry : mHuns) { - if (entry.getKey().equals(key)) return false; - } - return true; - }); - // THEN only the current HUN, mEntry, should be lifetimeExtended - assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, /* cancellationReason */ 0)); - assertFalse(mNotifLifetimeExtender.maybeExtendLifetime( - new NotificationEntryBuilder() - .setPkg("test-package") - .build(), /* cancellationReason */ 0)); - } - - @Test - public void testShowHUNOnInflationFinished() { - // WHEN a notification should HUN and its inflation is finished - when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true); - - ArgumentCaptor bindCallbackCaptor = - ArgumentCaptor.forClass(BindCallback.class); - mCollectionListener.onEntryAdded(mEntry); - verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), bindCallbackCaptor.capture()); - - bindCallbackCaptor.getValue().onBindFinished(mEntry); - - // THEN we tell the HeadsUpManager to show the notification - verify(mHeadsUpManager).showNotification(mEntry); - } - - @Test - public void testNoHUNOnInflationFinished() { - // WHEN a notification shouldn't HUN and its inflation is finished - when(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false); - ArgumentCaptor bindCallbackCaptor = - ArgumentCaptor.forClass(BindCallback.class); - mCollectionListener.onEntryAdded(mEntry); - - // THEN we never bind the heads up view or tell HeadsUpManager to show the notification - verify(mHeadsUpViewBinder, never()).bindHeadsUpView( - eq(mEntry), bindCallbackCaptor.capture()); - verify(mHeadsUpManager, never()).showNotification(mEntry); - } - - @Test - public void testOnEntryRemovedRemovesHeadsUpNotification() { - // GIVEN the current HUN is mEntry - addHUN(mEntry); - - // WHEN mEntry is removed from the notification collection - mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0); - when(mRemoteInputManager.isSpinning(any())).thenReturn(false); - - // THEN heads up manager should remove the entry - verify(mHeadsUpManager).removeNotification(mEntry.getKey(), false); - } - - private void addHUN(NotificationEntry entry) { - mHuns.add(entry); - when(mHeadsUpManager.getTopEntry()).thenReturn(entry); - mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, entry != null); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt new file mode 100644 index 0000000000000..c67a2331b023d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2020 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.collection.coordinator + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback +import com.android.systemui.statusbar.notification.collection.render.NodeController +import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder +import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider +import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyString +import org.mockito.BDDMockito.given +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.util.ArrayList +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class HeadsUpCoordinatorTest : SysuiTestCase() { + private lateinit var mCoordinator: HeadsUpCoordinator + + // captured listeners and pluggables: + private lateinit var mCollectionListener: NotifCollectionListener + private lateinit var mNotifPromoter: NotifPromoter + private lateinit var mNotifLifetimeExtender: NotifLifetimeExtender + private lateinit var mOnHeadsUpChangedListener: OnHeadsUpChangedListener + private lateinit var mNotifSectioner: NotifSectioner + + private val mNotifPipeline: NotifPipeline = mock() + private val mHeadsUpManager: HeadsUpManager = mock() + private val mHeadsUpViewBinder: HeadsUpViewBinder = mock() + private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider = mock() + private val mRemoteInputManager: NotificationRemoteInputManager = mock() + private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock() + private val mHeaderController: NodeController = mock() + + private lateinit var mEntry: NotificationEntry + private val mExecutor = FakeExecutor(FakeSystemClock()) + private val mHuns: ArrayList = ArrayList() + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mCoordinator = HeadsUpCoordinator( + mHeadsUpManager, + mHeadsUpViewBinder, + mNotificationInterruptStateProvider, + mRemoteInputManager, + mHeaderController, + mExecutor) + mCoordinator.attach(mNotifPipeline) + + // capture arguments: + mCollectionListener = withArgCaptor { + verify(mNotifPipeline).addCollectionListener(capture()) + } + mNotifPromoter = withArgCaptor { + verify(mNotifPipeline).addPromoter(capture()) + } + mNotifLifetimeExtender = withArgCaptor { + verify(mNotifPipeline).addNotificationLifetimeExtender(capture()) + } + mOnHeadsUpChangedListener = withArgCaptor { + verify(mHeadsUpManager).addListener(capture()) + } + given(mHeadsUpManager.allEntries).willAnswer { mHuns.stream() } + given(mHeadsUpManager.isAlerting(anyString())).willAnswer { invocation -> + val key = invocation.getArgument(0) + mHuns.any { entry -> entry.key == key } + } + given(mHeadsUpManager.canRemoveImmediately(anyString())).willAnswer { invocation -> + val key = invocation.getArgument(0) + !mHuns.any { entry -> entry.key == key } + } + whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L) + mNotifSectioner = mCoordinator.sectioner + mNotifLifetimeExtender.setCallback(mEndLifetimeExtension) + mEntry = NotificationEntryBuilder().build() + } + + @Test + fun testCancelStickyNotification() { + whenever(mHeadsUpManager.isSticky(anyString())).thenReturn(true) + addHUN(mEntry) + whenever(mHeadsUpManager.canRemoveImmediately(anyString())).thenReturn(false, true) + whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 0L) + assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0)) + mExecutor.advanceClockToLast() + mExecutor.runAllReady() + verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(false)) + verify(mHeadsUpManager, times(1)).removeNotification(anyString(), eq(true)) + } + + @Test + fun testCancelUpdatedStickyNotification() { + whenever(mHeadsUpManager.isSticky(anyString())).thenReturn(true) + addHUN(mEntry) + whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L) + assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0)) + mExecutor.advanceClockToLast() + mExecutor.runAllReady() + verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(false)) + verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(true)) + } + + @Test + fun testCancelNotification() { + whenever(mHeadsUpManager.isSticky(anyString())).thenReturn(false) + addHUN(mEntry) + whenever(mHeadsUpManager.getEarliestRemovalTime(anyString())).thenReturn(1000L, 500L) + assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, 0)) + mExecutor.advanceClockToLast() + mExecutor.runAllReady() + verify(mHeadsUpManager, times(1)).removeNotification(anyString(), eq(false)) + verify(mHeadsUpManager, times(0)).removeNotification(anyString(), eq(true)) + } + + @Test + fun testPromotesCurrentHUN() { + // GIVEN the current HUN is set to mEntry + addHUN(mEntry) + + // THEN only promote the current HUN, mEntry + assertTrue(mNotifPromoter.shouldPromoteToTopLevel(mEntry)) + assertFalse(mNotifPromoter.shouldPromoteToTopLevel(NotificationEntryBuilder() + .setPkg("test-package2") + .build())) + } + + @Test + fun testIncludeInSectionCurrentHUN() { + // GIVEN the current HUN is set to mEntry + addHUN(mEntry) + + // THEN only section the current HUN, mEntry + assertTrue(mNotifSectioner.isInSection(mEntry)) + assertFalse(mNotifSectioner.isInSection(NotificationEntryBuilder() + .setPkg("test-package") + .build())) + } + + @Test + fun testLifetimeExtendsCurrentHUN() { + // GIVEN there is a HUN, mEntry + addHUN(mEntry) + + // THEN only the current HUN, mEntry, should be lifetimeExtended + assertTrue(mNotifLifetimeExtender.maybeExtendLifetime(mEntry, /* cancellationReason */ 0)) + assertFalse(mNotifLifetimeExtender.maybeExtendLifetime( + NotificationEntryBuilder() + .setPkg("test-package") + .build(), /* cancellationReason */ 0)) + } + + @Test + fun testShowHUNOnInflationFinished() { + // WHEN a notification should HUN and its inflation is finished + whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true) + + mCollectionListener.onEntryAdded(mEntry) + withArgCaptor { + verify(mHeadsUpViewBinder).bindHeadsUpView(eq(mEntry), capture()) + }.onBindFinished(mEntry) + + // THEN we tell the HeadsUpManager to show the notification + verify(mHeadsUpManager).showNotification(mEntry) + } + + @Test + fun testNoHUNOnInflationFinished() { + // WHEN a notification shouldn't HUN and its inflation is finished + whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(false) + mCollectionListener.onEntryAdded(mEntry) + + // THEN we never bind the heads up view or tell HeadsUpManager to show the notification + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mEntry), any()) + verify(mHeadsUpManager, never()).showNotification(mEntry) + } + + @Test + fun testOnEntryRemovedRemovesHeadsUpNotification() { + // GIVEN the current HUN is mEntry + addHUN(mEntry) + + // WHEN mEntry is removed from the notification collection + mCollectionListener.onEntryRemoved(mEntry, /* cancellation reason */ 0) + whenever(mRemoteInputManager.isSpinning(any())).thenReturn(false) + + // THEN heads up manager should remove the entry + verify(mHeadsUpManager).removeNotification(mEntry.key, false) + } + + private fun addHUN(entry: NotificationEntry) { + mHuns.add(entry) + whenever(mHeadsUpManager.topEntry).thenReturn(entry) + mOnHeadsUpChangedListener.onHeadsUpStateChanged(entry, true) + } +} \ No newline at end of file