Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt 0 → 100644 +70 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.chips.notification.domain.interactor import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(StatusBarNotifChips.FLAG_NAME) class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val underTest = kosmos.statusBarNotificationChipsInteractor @Test fun onPromotedNotificationChipTapped_emitsKeys() = testScope.runTest { val latest by collectValues(underTest.promotedNotificationChipTapEvent) underTest.onPromotedNotificationChipTapped("fakeKey") assertThat(latest).hasSize(1) assertThat(latest[0]).isEqualTo("fakeKey") underTest.onPromotedNotificationChipTapped("fakeKey2") assertThat(latest).hasSize(2) assertThat(latest[1]).isEqualTo("fakeKey2") } @Test fun onPromotedNotificationChipTapped_sameKeyTwice_emitsTwice() = testScope.runTest { val latest by collectValues(underTest.promotedNotificationChipTapEvent) underTest.onPromotedNotificationChipTapped("fakeKey") underTest.onPromotedNotificationChipTapped("fakeKey") assertThat(latest).hasSize(2) assertThat(latest[0]).isEqualTo("fakeKey") assertThat(latest[1]).isEqualTo("fakeKey") } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +26 −0 Original line number Diff line number Diff line Loading @@ -17,12 +17,14 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.notification.data.model.activeNotificationModel Loading Loading @@ -102,6 +104,30 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest!![1], secondIcon) } @Test fun chips_clickingChipNotifiesInteractor() = testScope.runTest { val latest by collectLastValue(underTest.chips) val latestChipTap by collectLastValue( kosmos.statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent ) setNotifs( listOf( activeNotificationModel( key = "clickTest", statusBarChipIcon = mock<StatusBarIconView>(), ) ) ) val chip = latest!![0] chip.onClickListener!!.onClick(mock<View>()) assertThat(latestChipTap).isEqualTo("clickTest") } private fun setNotifs(notifs: List<ActiveNotificationModel>) { activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +119 −11 Original line number Diff line number Diff line Loading @@ -17,12 +17,18 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification.GROUP_ALERT_ALL import android.app.Notification.GROUP_ALERT_SUMMARY import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.HeadsUpManagerPhone import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline Loading @@ -32,6 +38,8 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener 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.mockNotifCollection import com.android.systemui.statusbar.notification.collection.notifCollection 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 Loading @@ -43,9 +51,9 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback import com.android.systemui.statusbar.notification.HeadsUpManagerPhone import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq Loading @@ -54,6 +62,7 @@ import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock import java.util.ArrayList import java.util.function.Consumer import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue Loading @@ -73,6 +82,11 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @RunWithLooper class HeadsUpCoordinatorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val statusBarNotificationChipsInteractor = kosmos.statusBarNotificationChipsInteractor private val notifCollection = kosmos.mockNotifCollection private lateinit var coordinator: HeadsUpCoordinator // captured listeners and pluggables: Loading Loading @@ -115,16 +129,19 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { helper = NotificationGroupTestHelper(mContext) coordinator = HeadsUpCoordinator( kosmos.applicationCoroutineScope, logger, systemClock, notifCollection, headsUpManager, headsUpViewBinder, visualInterruptionDecisionProvider, remoteInputManager, launchFullScreenIntentProvider, flags, statusBarNotificationChipsInteractor, headerController, executor executor, ) coordinator.attach(notifPipeline) Loading Loading @@ -351,7 +368,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { assertFalse( notifLifetimeExtender.maybeExtendLifetime( NotificationEntryBuilder().setPkg("test-package").build(), /* reason= */ 0 /* reason= */ 0, ) ) } Loading Loading @@ -441,6 +458,97 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { notifLifetimeExtender.cancelLifetimeExtension(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_hasNotifEntry_shownAsHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) executor.advanceClockToLast() executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) finishBind(entry) verify(headsUpManager).showNotification(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_noNotifEntry_noHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(null) statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) executor.advanceClockToLast() executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any()) verify(headsUpManager, never()).showNotification(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) // First, add the entry as shouldn't HUN setShouldHeadsUp(entry, false) collectionListener.onEntryAdded(entry) beforeTransformGroupsListener.onBeforeTransformGroups(listOf(entry)) beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) // WHEN that entry becomes a promoted notification and is tapped statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) executor.advanceClockToLast() executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) // THEN it's still shown as heads up finishBind(entry) verify(headsUpManager).showNotification(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = testScope.runTest { // First, the promoted notification appears as not heads up val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() whenever(notifCollection.getEntry(promotedEntry.key)).thenReturn(promotedEntry) setShouldHeadsUp(promotedEntry, false) collectionListener.onEntryAdded(promotedEntry) beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry)) beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry)) verify(headsUpViewBinder, never()).bindHeadsUpView(eq(promotedEntry), any()) verify(headsUpManager, never()).showNotification(promotedEntry) // Then a new notification comes in that should be heads up setShouldHeadsUp(entry, false) whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) collectionListener.onEntryAdded(entry) // At the same time, the promoted notification chip is tapped statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(promotedEntry.key) executor.advanceClockToLast() executor.runAllReady() // WHEN we finalize the pipeline beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry, entry)) beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry, entry)) // THEN the promoted entry is shown as a HUN, *not* the new entry finishBind(promotedEntry) verify(headsUpManager).showNotification(promotedEntry) verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any()) verify(headsUpManager, never()).showNotification(entry) } @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) Loading Loading @@ -862,7 +970,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider).launchFullScreenIntent(entry) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) } Loading @@ -885,7 +993,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) } Loading @@ -899,7 +1007,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) clearInterruptionProviderInvocations() Loading @@ -917,7 +1025,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(headsUpManager, never()).showNotification(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) clearInterruptionProviderInvocations() Loading @@ -942,7 +1050,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) clearInterruptionProviderInvocations() Loading Loading @@ -975,7 +1083,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(headsUpManager, never()).showNotification(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) clearInterruptionProviderInvocations() } Loading Loading @@ -1070,7 +1178,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private fun setShouldFullScreen( entry: NotificationEntry, originalDecision: FullScreenIntentDecision originalDecision: FullScreenIntentDecision, ) { whenever(visualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)) .thenAnswer { FullScreenIntentDecisionImpl(entry, originalDecision) } Loading @@ -1078,7 +1186,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private fun verifyLoggedFullScreenIntentDecision( entry: NotificationEntry, originalDecision: FullScreenIntentDecision originalDecision: FullScreenIntentDecision, ) { val decision = withArgCaptor { verify(visualInterruptionDecisionProvider).logFullScreenIntentDecision(capture()) Loading packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.chips.notification.domain.interactor import android.annotation.SuppressLint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow /** An interactor for the notification chips shown in the status bar. */ @SysUISingleton class StatusBarNotificationChipsInteractor @Inject constructor() { // Each chip tap is an individual event, *not* a state, which is why we're using SharedFlow not // StateFlow. There shouldn't be multiple updates per frame, which should avoid performance // problems. @SuppressLint("SharedFlowCreation") private val _promotedNotificationChipTapEvent = MutableSharedFlow<String>() /** * SharedFlow that emits each time a promoted notification's status bar chip is tapped. The * emitted value is the promoted notification's key. */ val promotedNotificationChipTapEvent: SharedFlow<String> = _promotedNotificationChipTapEvent.asSharedFlow() suspend fun onPromotedNotificationChipTapped(key: String) { StatusBarNotifChips.assertInNewMode() _promotedNotificationChipTapEvent.emit(key) } } packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +20 −3 Original line number Diff line number Diff line Loading @@ -16,20 +16,30 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** A view model for status bar chips for promoted ongoing notifications. */ @SysUISingleton class NotifChipsViewModel @Inject constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { constructor( @Application private val applicationScope: CoroutineScope, activeNotificationsInteractor: ActiveNotificationsInteractor, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, ) { /** * A flow modeling the notification chips that should be shown. Emits an empty list if there are * no notifications that should show a status bar chip. Loading @@ -44,13 +54,20 @@ constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { * notification has invalid data such that it can't be displayed as a chip. */ private fun ActiveNotificationModel.toChipModel(): OngoingActivityChipModel.Shown? { StatusBarNotifChips.assertInNewMode() // TODO(b/364653005): Log error if there's no icon view. val rawIcon = this.statusBarChipIconView ?: return null val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(rawIcon) // TODO(b/364653005): Use the notification color if applicable. val colors = ColorsModel.Themed // TODO(b/364653005): When the chip is clicked, show the HUN. val onClickListener = null val onClickListener = View.OnClickListener { // The notification pipeline needs everything to run on the main thread, so keep // this event on the main thread. applicationScope.launch { notifChipsInteractor.onPromotedNotificationChipTapped(this@toChipModel.key) } } return OngoingActivityChipModel.Shown.ShortTimeDelta( icon, colors, Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt 0 → 100644 +70 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.chips.notification.domain.interactor import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(StatusBarNotifChips.FLAG_NAME) class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val underTest = kosmos.statusBarNotificationChipsInteractor @Test fun onPromotedNotificationChipTapped_emitsKeys() = testScope.runTest { val latest by collectValues(underTest.promotedNotificationChipTapEvent) underTest.onPromotedNotificationChipTapped("fakeKey") assertThat(latest).hasSize(1) assertThat(latest[0]).isEqualTo("fakeKey") underTest.onPromotedNotificationChipTapped("fakeKey2") assertThat(latest).hasSize(2) assertThat(latest[1]).isEqualTo("fakeKey2") } @Test fun onPromotedNotificationChipTapped_sameKeyTwice_emitsTwice() = testScope.runTest { val latest by collectValues(underTest.promotedNotificationChipTapEvent) underTest.onPromotedNotificationChipTapped("fakeKey") underTest.onPromotedNotificationChipTapped("fakeKey") assertThat(latest).hasSize(2) assertThat(latest[0]).isEqualTo("fakeKey") assertThat(latest[1]).isEqualTo("fakeKey") } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +26 −0 Original line number Diff line number Diff line Loading @@ -17,12 +17,14 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.notification.data.model.activeNotificationModel Loading Loading @@ -102,6 +104,30 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertIsNotifChip(latest!![1], secondIcon) } @Test fun chips_clickingChipNotifiesInteractor() = testScope.runTest { val latest by collectLastValue(underTest.chips) val latestChipTap by collectLastValue( kosmos.statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent ) setNotifs( listOf( activeNotificationModel( key = "clickTest", statusBarChipIcon = mock<StatusBarIconView>(), ) ) ) val chip = latest!![0] chip.onClickListener!!.onClick(mock<View>()) assertThat(latestChipTap).isEqualTo("clickTest") } private fun setNotifs(notifs: List<ActiveNotificationModel>) { activeNotificationListRepository.activeNotifications.value = ActiveNotificationsStore.Builder() Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +119 −11 Original line number Diff line number Diff line Loading @@ -17,12 +17,18 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.app.Notification.GROUP_ALERT_ALL import android.app.Notification.GROUP_ALERT_SUMMARY import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.HeadsUpManagerPhone import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline Loading @@ -32,6 +38,8 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener 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.mockNotifCollection import com.android.systemui.statusbar.notification.collection.notifCollection 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 Loading @@ -43,9 +51,9 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback import com.android.systemui.statusbar.notification.HeadsUpManagerPhone import com.android.systemui.statusbar.phone.NotificationGroupTestHelper import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq Loading @@ -54,6 +62,7 @@ import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.FakeSystemClock import java.util.ArrayList import java.util.function.Consumer import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue Loading @@ -73,6 +82,11 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @RunWithLooper class HeadsUpCoordinatorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val statusBarNotificationChipsInteractor = kosmos.statusBarNotificationChipsInteractor private val notifCollection = kosmos.mockNotifCollection private lateinit var coordinator: HeadsUpCoordinator // captured listeners and pluggables: Loading Loading @@ -115,16 +129,19 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { helper = NotificationGroupTestHelper(mContext) coordinator = HeadsUpCoordinator( kosmos.applicationCoroutineScope, logger, systemClock, notifCollection, headsUpManager, headsUpViewBinder, visualInterruptionDecisionProvider, remoteInputManager, launchFullScreenIntentProvider, flags, statusBarNotificationChipsInteractor, headerController, executor executor, ) coordinator.attach(notifPipeline) Loading Loading @@ -351,7 +368,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { assertFalse( notifLifetimeExtender.maybeExtendLifetime( NotificationEntryBuilder().setPkg("test-package").build(), /* reason= */ 0 /* reason= */ 0, ) ) } Loading Loading @@ -441,6 +458,97 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { notifLifetimeExtender.cancelLifetimeExtension(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_hasNotifEntry_shownAsHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) executor.advanceClockToLast() executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) finishBind(entry) verify(headsUpManager).showNotification(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_noNotifEntry_noHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(null) statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) executor.advanceClockToLast() executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any()) verify(headsUpManager, never()).showNotification(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) // First, add the entry as shouldn't HUN setShouldHeadsUp(entry, false) collectionListener.onEntryAdded(entry) beforeTransformGroupsListener.onBeforeTransformGroups(listOf(entry)) beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) // WHEN that entry becomes a promoted notification and is tapped statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) executor.advanceClockToLast() executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) // THEN it's still shown as heads up finishBind(entry) verify(headsUpManager).showNotification(entry) } @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = testScope.runTest { // First, the promoted notification appears as not heads up val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() whenever(notifCollection.getEntry(promotedEntry.key)).thenReturn(promotedEntry) setShouldHeadsUp(promotedEntry, false) collectionListener.onEntryAdded(promotedEntry) beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry)) beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry)) verify(headsUpViewBinder, never()).bindHeadsUpView(eq(promotedEntry), any()) verify(headsUpManager, never()).showNotification(promotedEntry) // Then a new notification comes in that should be heads up setShouldHeadsUp(entry, false) whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) collectionListener.onEntryAdded(entry) // At the same time, the promoted notification chip is tapped statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(promotedEntry.key) executor.advanceClockToLast() executor.runAllReady() // WHEN we finalize the pipeline beforeTransformGroupsListener.onBeforeTransformGroups(listOf(promotedEntry, entry)) beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(promotedEntry, entry)) // THEN the promoted entry is shown as a HUN, *not* the new entry finishBind(promotedEntry) verify(headsUpManager).showNotification(promotedEntry) verify(headsUpViewBinder, never()).bindHeadsUpView(eq(entry), any()) verify(headsUpManager, never()).showNotification(entry) } @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) Loading Loading @@ -862,7 +970,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider).launchFullScreenIntent(entry) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) } Loading @@ -885,7 +993,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) } Loading @@ -899,7 +1007,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) clearInterruptionProviderInvocations() Loading @@ -917,7 +1025,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(headsUpManager, never()).showNotification(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) clearInterruptionProviderInvocations() Loading @@ -942,7 +1050,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND, ) clearInterruptionProviderInvocations() Loading Loading @@ -975,7 +1083,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { verify(headsUpManager, never()).showNotification(any()) verifyLoggedFullScreenIntentDecision( entry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE, ) clearInterruptionProviderInvocations() } Loading Loading @@ -1070,7 +1178,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private fun setShouldFullScreen( entry: NotificationEntry, originalDecision: FullScreenIntentDecision originalDecision: FullScreenIntentDecision, ) { whenever(visualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)) .thenAnswer { FullScreenIntentDecisionImpl(entry, originalDecision) } Loading @@ -1078,7 +1186,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private fun verifyLoggedFullScreenIntentDecision( entry: NotificationEntry, originalDecision: FullScreenIntentDecision originalDecision: FullScreenIntentDecision, ) { val decision = withArgCaptor { verify(visualInterruptionDecisionProvider).logFullScreenIntentDecision(capture()) Loading
packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.chips.notification.domain.interactor import android.annotation.SuppressLint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow /** An interactor for the notification chips shown in the status bar. */ @SysUISingleton class StatusBarNotificationChipsInteractor @Inject constructor() { // Each chip tap is an individual event, *not* a state, which is why we're using SharedFlow not // StateFlow. There shouldn't be multiple updates per frame, which should avoid performance // problems. @SuppressLint("SharedFlowCreation") private val _promotedNotificationChipTapEvent = MutableSharedFlow<String>() /** * SharedFlow that emits each time a promoted notification's status bar chip is tapped. The * emitted value is the promoted notification's key. */ val promotedNotificationChipTapEvent: SharedFlow<String> = _promotedNotificationChipTapEvent.asSharedFlow() suspend fun onPromotedNotificationChipTapped(key: String) { StatusBarNotifChips.assertInNewMode() _promotedNotificationChipTapEvent.emit(key) } }
packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +20 −3 Original line number Diff line number Diff line Loading @@ -16,20 +16,30 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** A view model for status bar chips for promoted ongoing notifications. */ @SysUISingleton class NotifChipsViewModel @Inject constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { constructor( @Application private val applicationScope: CoroutineScope, activeNotificationsInteractor: ActiveNotificationsInteractor, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, ) { /** * A flow modeling the notification chips that should be shown. Emits an empty list if there are * no notifications that should show a status bar chip. Loading @@ -44,13 +54,20 @@ constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { * notification has invalid data such that it can't be displayed as a chip. */ private fun ActiveNotificationModel.toChipModel(): OngoingActivityChipModel.Shown? { StatusBarNotifChips.assertInNewMode() // TODO(b/364653005): Log error if there's no icon view. val rawIcon = this.statusBarChipIconView ?: return null val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(rawIcon) // TODO(b/364653005): Use the notification color if applicable. val colors = ColorsModel.Themed // TODO(b/364653005): When the chip is clicked, show the HUN. val onClickListener = null val onClickListener = View.OnClickListener { // The notification pipeline needs everything to run on the main thread, so keep // this event on the main thread. applicationScope.launch { notifChipsInteractor.onPromotedNotificationChipTapped(this@toChipModel.key) } } return OngoingActivityChipModel.Shown.ShortTimeDelta( icon, colors, Loading