Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/NotificationContainerInteractorTest.kt 0 → 100644 +136 −0 Original line number Diff line number Diff line /* * 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.stack.domain import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationContainerInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer class NotificationContainerInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest get() = kosmos.notificationContainerInteractor private fun expandShade() { kosmos.sceneInteractor.snapToScene(Scenes.Shade, "test") kosmos.sceneInteractor.setTransitionState( flowOf(ObservableTransitionState.Idle(Scenes.Shade)) ) } private fun collapseShade() { kosmos.sceneInteractor.snapToScene(Scenes.Lockscreen, "test") kosmos.sceneInteractor.setTransitionState( flowOf(ObservableTransitionState.Idle(Scenes.Lockscreen)) ) } private fun pinNotif() { kosmos.headsUpNotificationRepository.setNotifications( listOf( UnconfinedFakeHeadsUpRowRepository( key = "key", pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), ) ) ) } @Test fun activate_collapsed_noUnpinAll() = kosmos.runTest { // GIVEN one pinned notif in COLLAPSED shade pinNotif() collapseShade() // WHEN the interactor is activated underTest.activateIn(testScope) // THEN unpinAll is NOT called because there was no false->true transition assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.PinnedByUser) } @Test fun activate_expanded_noUnpinAll() = kosmos.runTest { // GIVEN one pinned notif in EXPANDED shade pinNotif() expandShade() // WHEN the interactor is activated underTest.activateIn(testScope) // THEN unpinAll is NOT called because there was no false->true transition assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.PinnedByUser) } @Test fun activate_collapsedToExpanded_unpinAll() = kosmos.runTest { // GIVEN one pinned notif in COLLAPSED shade pinNotif() collapseShade() underTest.activateIn(testScope) // WHEN the shade EXPANDS expandShade() // THEN unpinned IS called assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.NotPinned) } @Test fun activate_expandedToCollapsed_noUnpin() = kosmos.runTest { // GIVEN one pinned notif in EXPANDED shade pinNotif() expandShade() underTest.activateIn(testScope) // WHEN the shade COLLAPSES collapseShade() // THEN unpinned is NOT called assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.PinnedByUser) } } packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +5 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.qs.panels.ui.viewmodel.AnimateQsTilesViewModel import com.android.systemui.statusbar.notification.domain.interactor.NotificationContainerInteractor import com.android.systemui.scene.domain.interactor.OnBootTransitionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger Loading Loading @@ -69,6 +70,7 @@ constructor( private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, private val onBootTransitionInteractor: OnBootTransitionInteractor, private val notificationContainerInteractor: NotificationContainerInteractor, shadeModeInteractor: ShadeModeInteractor, private val remoteInputInteractor: RemoteInputInteractor, private val logger: SceneLogger, Loading Loading @@ -145,6 +147,9 @@ constructor( coroutineScope { launch { hydrator.activate() } launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() } launch("NotificationContainerInteractor") { notificationContainerInteractor.activate() } } awaitCancellation() } finally { Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationContainerInteractor.kt 0 → 100644 +47 −0 Original line number Diff line number Diff line /* * 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.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.pairwise import kotlinx.coroutines.flow.filter import javax.inject.Inject /** * Manages notification-related logic that needs to persist across scene changes within the scene * container. */ @SysUISingleton class NotificationContainerInteractor @Inject constructor( private val shadeInteractor: ShadeInteractor, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, ) : ExclusiveActivatable() { override suspend fun onActivated() { // Unpin all HUNs only when the shade transitions from closed to open. shadeInteractor.isAnyExpanded .pairwise() .filter { (wasExpanded, isExpanded) -> !wasExpanded && isExpanded } .collect { headsUpNotificationInteractor.unpinAll(true) } } } packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +0 −6 Original line number Diff line number Diff line Loading @@ -115,12 +115,6 @@ constructor( coroutineScope { launch { hydrator.activate() } launch(context = mainContext) { shadeInteractor.isAnyExpanded .filter { it } .collect { headsUpNotificationInteractor.unpinAll(true) } } launch { sceneInteractor.transitionState .filter { it is ObservableTransitionState.Idle } Loading packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +2 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import com.android.systemui.scene.ui.viewmodel.dualShadeEducationalTooltipsViewM import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationContainerInteractor import com.android.systemui.wallpapers.ui.viewmodel.wallpaperViewModel import kotlinx.coroutines.flow.MutableStateFlow import org.mockito.kotlin.mock Loading Loading @@ -113,6 +114,7 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { burnIn = aodBurnInViewModel, clock = keyguardClockViewModel, onBootTransitionInteractor = onBootTransitionInteractor, notificationContainerInteractor = notificationContainerInteractor, dualShadeEducationalTooltipsViewModelFactory = dualShadeEducationalTooltipsViewModelFactory, animateQsTilesViewModelFactory = animateQsTilesViewModelFactory, Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/NotificationContainerInteractorTest.kt 0 → 100644 +136 −0 Original line number Diff line number Diff line /* * 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.stack.domain import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.notification.data.repository.UnconfinedFakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationContainerInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer class NotificationContainerInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest get() = kosmos.notificationContainerInteractor private fun expandShade() { kosmos.sceneInteractor.snapToScene(Scenes.Shade, "test") kosmos.sceneInteractor.setTransitionState( flowOf(ObservableTransitionState.Idle(Scenes.Shade)) ) } private fun collapseShade() { kosmos.sceneInteractor.snapToScene(Scenes.Lockscreen, "test") kosmos.sceneInteractor.setTransitionState( flowOf(ObservableTransitionState.Idle(Scenes.Lockscreen)) ) } private fun pinNotif() { kosmos.headsUpNotificationRepository.setNotifications( listOf( UnconfinedFakeHeadsUpRowRepository( key = "key", pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), ) ) ) } @Test fun activate_collapsed_noUnpinAll() = kosmos.runTest { // GIVEN one pinned notif in COLLAPSED shade pinNotif() collapseShade() // WHEN the interactor is activated underTest.activateIn(testScope) // THEN unpinAll is NOT called because there was no false->true transition assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.PinnedByUser) } @Test fun activate_expanded_noUnpinAll() = kosmos.runTest { // GIVEN one pinned notif in EXPANDED shade pinNotif() expandShade() // WHEN the interactor is activated underTest.activateIn(testScope) // THEN unpinAll is NOT called because there was no false->true transition assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.PinnedByUser) } @Test fun activate_collapsedToExpanded_unpinAll() = kosmos.runTest { // GIVEN one pinned notif in COLLAPSED shade pinNotif() collapseShade() underTest.activateIn(testScope) // WHEN the shade EXPANDS expandShade() // THEN unpinned IS called assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.NotPinned) } @Test fun activate_expandedToCollapsed_noUnpin() = kosmos.runTest { // GIVEN one pinned notif in EXPANDED shade pinNotif() expandShade() underTest.activateIn(testScope) // WHEN the shade COLLAPSES collapseShade() // THEN unpinned is NOT called assertThat(headsUpNotificationRepository.orderedHeadsUpRows.value[0].pinnedStatus.value) .isEqualTo(PinnedStatus.PinnedByUser) } }
packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +5 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.qs.panels.ui.viewmodel.AnimateQsTilesViewModel import com.android.systemui.statusbar.notification.domain.interactor.NotificationContainerInteractor import com.android.systemui.scene.domain.interactor.OnBootTransitionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger Loading Loading @@ -69,6 +70,7 @@ constructor( private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, private val onBootTransitionInteractor: OnBootTransitionInteractor, private val notificationContainerInteractor: NotificationContainerInteractor, shadeModeInteractor: ShadeModeInteractor, private val remoteInputInteractor: RemoteInputInteractor, private val logger: SceneLogger, Loading Loading @@ -145,6 +147,9 @@ constructor( coroutineScope { launch { hydrator.activate() } launch("SceneContainerHapticsViewModel") { hapticsViewModel.activate() } launch("NotificationContainerInteractor") { notificationContainerInteractor.activate() } } awaitCancellation() } finally { Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationContainerInteractor.kt 0 → 100644 +47 −0 Original line number Diff line number Diff line /* * 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.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.pairwise import kotlinx.coroutines.flow.filter import javax.inject.Inject /** * Manages notification-related logic that needs to persist across scene changes within the scene * container. */ @SysUISingleton class NotificationContainerInteractor @Inject constructor( private val shadeInteractor: ShadeInteractor, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, ) : ExclusiveActivatable() { override suspend fun onActivated() { // Unpin all HUNs only when the shade transitions from closed to open. shadeInteractor.isAnyExpanded .pairwise() .filter { (wasExpanded, isExpanded) -> !wasExpanded && isExpanded } .collect { headsUpNotificationInteractor.unpinAll(true) } } }
packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +0 −6 Original line number Diff line number Diff line Loading @@ -115,12 +115,6 @@ constructor( coroutineScope { launch { hydrator.activate() } launch(context = mainContext) { shadeInteractor.isAnyExpanded .filter { it } .collect { headsUpNotificationInteractor.unpinAll(true) } } launch { sceneInteractor.transitionState .filter { it is ObservableTransitionState.Idle } Loading
packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +2 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import com.android.systemui.scene.ui.viewmodel.dualShadeEducationalTooltipsViewM import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationContainerInteractor import com.android.systemui.wallpapers.ui.viewmodel.wallpaperViewModel import kotlinx.coroutines.flow.MutableStateFlow import org.mockito.kotlin.mock Loading Loading @@ -113,6 +114,7 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { burnIn = aodBurnInViewModel, clock = keyguardClockViewModel, onBootTransitionInteractor = onBootTransitionInteractor, notificationContainerInteractor = notificationContainerInteractor, dualShadeEducationalTooltipsViewModelFactory = dualShadeEducationalTooltipsViewModelFactory, animateQsTilesViewModelFactory = animateQsTilesViewModelFactory, Loading