Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt +25 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.qs.ui.viewmodel import android.platform.test.annotations.DisableFlags import android.testing.TestableLooper.RunWithLooper import androidx.lifecycle.LifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 Loading @@ -26,20 +27,26 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test Loading @@ -51,6 +58,7 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @RunWithLooper @EnableSceneContainer @DisableFlags(com.android.systemui.Flags.FLAG_DUAL_SHADE) class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() Loading @@ -64,6 +72,8 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { private val footerActionsController = mock<FooterActionsController>() private val sceneContainerStartable = kosmos.sceneContainerStartable private val sceneInteractor by lazy { kosmos.sceneInteractor } private val shadeInteractor by lazy { kosmos.shadeInteractor } private lateinit var underTest: QuickSettingsSceneContentViewModel Loading @@ -80,7 +90,10 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { footerActionsViewModelFactory = footerActionsViewModelFactory, footerActionsController = footerActionsController, mediaCarouselInteractor = kosmos.mediaCarouselInteractor, shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, ) underTest.activateIn(testScope) } @Test Loading Loading @@ -122,4 +135,16 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { assertThat(isMediaVisible).isTrue() } @Test fun shadeModeChange_switchToShadeScene() = testScope.runTest { val scene by collectLastValue(sceneInteractor.currentScene) // switch to split shade kosmos.shadeRepository.setShadeLayoutWide(true) runCurrent() assertThat(scene).isEqualTo(Scenes.Shade) } } packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt +24 −1 Original line number Diff line number Diff line Loading @@ -17,16 +17,24 @@ package com.android.systemui.qs.ui.viewmodel import androidx.lifecycle.LifecycleOwner import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** * Models UI state needed for rendering the content of the quick settings scene. Loading @@ -43,7 +51,9 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, val mediaCarouselInteractor: MediaCarouselInteractor, ) { private val shadeInteractor: ShadeInteractor, private val sceneInteractor: SceneInteractor, ) : ExclusiveActivatable() { val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasAnyMediaOrRecommendation Loading @@ -56,6 +66,19 @@ constructor( return footerActionsViewModelFactory.create(lifecycleOwner) } override suspend fun onActivated(): Nothing { coroutineScope { launch { shadeInteractor.shadeMode.collect { shadeMode -> if (shadeMode == ShadeMode.Split) { sceneInteractor.snapToScene(Scenes.Shade, "Unfold while on QS") } } } awaitCancellation() } } @AssistedFactory interface Factory { fun create(): QuickSettingsSceneContentViewModel Loading packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt +13 −18 Original line number Diff line number Diff line Loading @@ -58,29 +58,20 @@ constructor( fun onSceneChange(from: SceneKey, to: SceneKey) { check(from != to) { "from == to, from=${from.debugName}, to=${to.debugName}" } when (stackOperation(from, to)) { Clear -> { _backStack.value = sceneStackOf() } Push -> { _backStack.update { s -> s.push(from) } } Pop -> { _backStack.update { s -> checkNotNull(s.pop()) { "Cannot pop ${from.debugName} when stack is empty" } .also { val popped = s.peek() check(popped == to) { "Expected to pop ${to.debugName} but instead popped ${popped?.debugName}" } } } _backStack.update { stack -> when (stackOperation(from, to, stack)) { null -> stack Clear -> sceneStackOf() Push -> stack.push(from) Pop -> checkNotNull(stack.pop()) { "Cannot pop ${from.debugName} when stack is empty" } } } logger.logSceneBackStack(backStack.value.asIterable()) } private fun stackOperation(from: SceneKey, to: SceneKey): StackOperation { private fun stackOperation(from: SceneKey, to: SceneKey, stack: SceneStack): StackOperation? { val fromDistance = checkNotNull(sceneContainerConfig.navigationDistances[from]) { "No distance mapping for scene \"${from.debugName}\"!" Loading @@ -93,6 +84,7 @@ constructor( return when { toDistance == 0 -> Clear toDistance > fromDistance -> Push stack.peek() != to -> null toDistance < fromDistance -> Pop else -> error( Loading @@ -103,7 +95,10 @@ constructor( } private sealed interface StackOperation private data object Clear : StackOperation private data object Push : StackOperation private data object Pop : StackOperation } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt +25 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.qs.ui.viewmodel import android.platform.test.annotations.DisableFlags import android.testing.TestableLooper.RunWithLooper import androidx.lifecycle.LifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 Loading @@ -26,20 +27,26 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test Loading @@ -51,6 +58,7 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @RunWithLooper @EnableSceneContainer @DisableFlags(com.android.systemui.Flags.FLAG_DUAL_SHADE) class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() Loading @@ -64,6 +72,8 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { private val footerActionsController = mock<FooterActionsController>() private val sceneContainerStartable = kosmos.sceneContainerStartable private val sceneInteractor by lazy { kosmos.sceneInteractor } private val shadeInteractor by lazy { kosmos.shadeInteractor } private lateinit var underTest: QuickSettingsSceneContentViewModel Loading @@ -80,7 +90,10 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { footerActionsViewModelFactory = footerActionsViewModelFactory, footerActionsController = footerActionsController, mediaCarouselInteractor = kosmos.mediaCarouselInteractor, shadeInteractor = shadeInteractor, sceneInteractor = sceneInteractor, ) underTest.activateIn(testScope) } @Test Loading Loading @@ -122,4 +135,16 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { assertThat(isMediaVisible).isTrue() } @Test fun shadeModeChange_switchToShadeScene() = testScope.runTest { val scene by collectLastValue(sceneInteractor.currentScene) // switch to split shade kosmos.shadeRepository.setShadeLayoutWide(true) runCurrent() assertThat(scene).isEqualTo(Scenes.Shade) } }
packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt +24 −1 Original line number Diff line number Diff line Loading @@ -17,16 +17,24 @@ package com.android.systemui.qs.ui.viewmodel import androidx.lifecycle.LifecycleOwner import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** * Models UI state needed for rendering the content of the quick settings scene. Loading @@ -43,7 +51,9 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, val mediaCarouselInteractor: MediaCarouselInteractor, ) { private val shadeInteractor: ShadeInteractor, private val sceneInteractor: SceneInteractor, ) : ExclusiveActivatable() { val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasAnyMediaOrRecommendation Loading @@ -56,6 +66,19 @@ constructor( return footerActionsViewModelFactory.create(lifecycleOwner) } override suspend fun onActivated(): Nothing { coroutineScope { launch { shadeInteractor.shadeMode.collect { shadeMode -> if (shadeMode == ShadeMode.Split) { sceneInteractor.snapToScene(Scenes.Shade, "Unfold while on QS") } } } awaitCancellation() } } @AssistedFactory interface Factory { fun create(): QuickSettingsSceneContentViewModel Loading
packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt +13 −18 Original line number Diff line number Diff line Loading @@ -58,29 +58,20 @@ constructor( fun onSceneChange(from: SceneKey, to: SceneKey) { check(from != to) { "from == to, from=${from.debugName}, to=${to.debugName}" } when (stackOperation(from, to)) { Clear -> { _backStack.value = sceneStackOf() } Push -> { _backStack.update { s -> s.push(from) } } Pop -> { _backStack.update { s -> checkNotNull(s.pop()) { "Cannot pop ${from.debugName} when stack is empty" } .also { val popped = s.peek() check(popped == to) { "Expected to pop ${to.debugName} but instead popped ${popped?.debugName}" } } } _backStack.update { stack -> when (stackOperation(from, to, stack)) { null -> stack Clear -> sceneStackOf() Push -> stack.push(from) Pop -> checkNotNull(stack.pop()) { "Cannot pop ${from.debugName} when stack is empty" } } } logger.logSceneBackStack(backStack.value.asIterable()) } private fun stackOperation(from: SceneKey, to: SceneKey): StackOperation { private fun stackOperation(from: SceneKey, to: SceneKey, stack: SceneStack): StackOperation? { val fromDistance = checkNotNull(sceneContainerConfig.navigationDistances[from]) { "No distance mapping for scene \"${from.debugName}\"!" Loading @@ -93,6 +84,7 @@ constructor( return when { toDistance == 0 -> Clear toDistance > fromDistance -> Push stack.peek() != to -> null toDistance < fromDistance -> Pop else -> error( Loading @@ -103,7 +95,10 @@ constructor( } private sealed interface StackOperation private data object Clear : StackOperation private data object Push : StackOperation private data object Pop : StackOperation }