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