Loading packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +2 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, motionEventHandlerReceiver = {}, ) .apply { setTransitionState(transitionState) } } Loading Loading @@ -199,6 +200,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { shadeSceneContentViewModel.activateIn(testScope) shadeSceneActionsViewModel.activateIn(testScope) bouncerSceneContentViewModel.activateIn(testScope) sceneContainerViewModel.activateIn(testScope) assertWithMessage("Initial scene key mismatch!") .that(sceneContainerViewModel.currentScene.value) Loading packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +26 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,8 @@ * limitations under the License. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent Loading @@ -25,6 +27,7 @@ import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor Loading @@ -37,6 +40,8 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before Loading @@ -57,6 +62,9 @@ class SceneContainerViewModelTest : SysuiTestCase() { private lateinit var underTest: SceneContainerViewModel private lateinit var activationJob: Job private var motionEventHandler: SceneContainerViewModel.MotionEventHandler? = null @Before fun setUp() { underTest = Loading @@ -64,7 +72,25 @@ class SceneContainerViewModelTest : SysuiTestCase() { sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, motionEventHandlerReceiver = { motionEventHandler -> this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler }, ) activationJob = Job() underTest.activateIn(testScope, activationJob) } @Test fun activate_setsMotionEventHandler() = testScope.runTest { assertThat(motionEventHandler).isNotNull() } @Test fun deactivate_clearsMotionEventHandler() = testScope.runTest { activationJob.cancel() runCurrent() assertThat(motionEventHandler).isNull() } @Test Loading packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +8 −7 Original line number Diff line number Diff line Loading @@ -26,13 +26,12 @@ class SceneWindowRootView( attrs, ) { private lateinit var viewModel: SceneContainerViewModel private var motionEventHandler: SceneContainerViewModel.MotionEventHandler? = null // TODO(b/298525212): remove once Compose exposes window inset bounds. private val windowInsets: MutableStateFlow<WindowInsets?> = MutableStateFlow(null) fun init( viewModel: SceneContainerViewModel, viewModelFactory: SceneContainerViewModel.Factory, containerConfig: SceneContainerConfig, sharedNotificationContainer: SharedNotificationContainer, scenes: Set<Scene>, Loading @@ -40,11 +39,13 @@ class SceneWindowRootView( sceneDataSourceDelegator: SceneDataSourceDelegator, alternateBouncerDependencies: AlternateBouncerDependencies, ) { this.viewModel = viewModel setLayoutInsetsController(layoutInsetController) SceneWindowRootViewBinder.bind( view = this@SceneWindowRootView, viewModel = viewModel, viewModelFactory = viewModelFactory, motionEventHandlerReceiver = { motionEventHandler -> this.motionEventHandler = motionEventHandler }, windowInsets = windowInsets, containerConfig = containerConfig, sharedNotificationContainer = sharedNotificationContainer, Loading @@ -69,10 +70,10 @@ class SceneWindowRootView( } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { viewModel.onMotionEvent(ev) motionEventHandler?.onMotionEvent(ev) return super.dispatchTouchEvent(ev).also { TouchLogger.logDispatchTouch(TAG, ev, it) viewModel.onMotionEventComplete() motionEventHandler?.onMotionEventComplete() } } Loading packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +14 −8 Original line number Diff line number Diff line Loading @@ -29,8 +29,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.animation.scene.SceneKey import com.android.compose.theme.PlatformTheme import com.android.internal.policy.ScreenDecorationsUtils Loading @@ -39,7 +37,9 @@ import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider import com.android.systemui.keyguard.ui.composable.AlternateBouncer import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.viewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scene Loading @@ -51,6 +51,7 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map Loading @@ -63,7 +64,8 @@ object SceneWindowRootViewBinder { /** Binds between the view and view-model pertaining to a specific scene container. */ fun bind( view: ViewGroup, viewModel: SceneContainerViewModel, viewModelFactory: SceneContainerViewModel.Factory, motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit, windowInsets: StateFlow<WindowInsets?>, containerConfig: SceneContainerConfig, sharedNotificationContainer: SharedNotificationContainer, Loading @@ -85,8 +87,11 @@ object SceneWindowRootViewBinder { } view.repeatWhenAttached { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { view.viewModel( minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModelFactory.create(motionEventHandlerReceiver) }, ) { viewModel -> try { view.setViewTreeOnBackPressedDispatcherOwner( object : OnBackPressedDispatcherOwner { override val onBackPressedDispatcher = Loading Loading @@ -140,13 +145,14 @@ object SceneWindowRootViewBinder { onVisibilityChangedInternal(isVisible) } } } awaitCancellation() } finally { // Here when destroyed. view.removeAllViews() } } } } private fun createSceneContainerView( scope: CoroutineScope, Loading packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +48 −23 Original line number Diff line number Diff line Loading @@ -23,25 +23,26 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import javax.inject.Inject import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map /** Models UI state for the scene container. */ @SysUISingleton class SceneContainerViewModel @Inject @AssistedInject constructor( private val sceneInteractor: SceneInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, ) { @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : SysUiViewModel() { /** * Keys of all scenes in the container. * Loading @@ -56,6 +57,29 @@ constructor( /** Whether the container is visible. */ val isVisible: StateFlow<Boolean> = sceneInteractor.isVisible override suspend fun onActivated() { try { // Sends a MotionEventHandler to the owner of the view-model so they can report // MotionEvents into the view-model. motionEventHandlerReceiver( object : MotionEventHandler { override fun onMotionEvent(motionEvent: MotionEvent) { this@SceneContainerViewModel.onMotionEvent(motionEvent) } override fun onMotionEventComplete() { this@SceneContainerViewModel.onMotionEventComplete() } } ) awaitCancellation() } finally { // Clears the previously-sent MotionEventHandler so the owner of the view-model releases // their reference to it. motionEventHandlerReceiver(null) } } /** * Binds the given flow so the system remembers it. * Loading Loading @@ -136,21 +160,22 @@ constructor( } } private fun replaceSceneFamilies( destinationScenes: Map<UserAction, UserActionResult>, ): Flow<Map<UserAction, UserActionResult>> { return destinationScenes .mapValues { (_, actionResult) -> sceneInteractor.resolveSceneFamily(actionResult.toScene).map { scene -> actionResult.copy(toScene = scene) } /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */ interface MotionEventHandler { /** Notifies that a [MotionEvent] has occurred. */ fun onMotionEvent(motionEvent: MotionEvent) /** * Notifies that the previous [MotionEvent] reported by [onMotionEvent] has finished * processing. */ fun onMotionEventComplete() } .combineValueFlows() @AssistedFactory interface Factory { fun create( motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ): SceneContainerViewModel } } private fun <K, V> Map<K, Flow<V>>.combineValueFlows(): Flow<Map<K, V>> = combine( asIterable().map { (k, fv) -> fv.map { k to it } }, transform = Array<Pair<K, V>>::toMap, ) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +2 −0 Original line number Diff line number Diff line Loading @@ -133,6 +133,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, motionEventHandlerReceiver = {}, ) .apply { setTransitionState(transitionState) } } Loading Loading @@ -199,6 +200,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { shadeSceneContentViewModel.activateIn(testScope) shadeSceneActionsViewModel.activateIn(testScope) bouncerSceneContentViewModel.activateIn(testScope) sceneContainerViewModel.activateIn(testScope) assertWithMessage("Initial scene key mismatch!") .that(sceneContainerViewModel.currentScene.value) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +26 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,8 @@ * limitations under the License. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent Loading @@ -25,6 +27,7 @@ import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor Loading @@ -37,6 +40,8 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before Loading @@ -57,6 +62,9 @@ class SceneContainerViewModelTest : SysuiTestCase() { private lateinit var underTest: SceneContainerViewModel private lateinit var activationJob: Job private var motionEventHandler: SceneContainerViewModel.MotionEventHandler? = null @Before fun setUp() { underTest = Loading @@ -64,7 +72,25 @@ class SceneContainerViewModelTest : SysuiTestCase() { sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, motionEventHandlerReceiver = { motionEventHandler -> this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler }, ) activationJob = Job() underTest.activateIn(testScope, activationJob) } @Test fun activate_setsMotionEventHandler() = testScope.runTest { assertThat(motionEventHandler).isNotNull() } @Test fun deactivate_clearsMotionEventHandler() = testScope.runTest { activationJob.cancel() runCurrent() assertThat(motionEventHandler).isNull() } @Test Loading
packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +8 −7 Original line number Diff line number Diff line Loading @@ -26,13 +26,12 @@ class SceneWindowRootView( attrs, ) { private lateinit var viewModel: SceneContainerViewModel private var motionEventHandler: SceneContainerViewModel.MotionEventHandler? = null // TODO(b/298525212): remove once Compose exposes window inset bounds. private val windowInsets: MutableStateFlow<WindowInsets?> = MutableStateFlow(null) fun init( viewModel: SceneContainerViewModel, viewModelFactory: SceneContainerViewModel.Factory, containerConfig: SceneContainerConfig, sharedNotificationContainer: SharedNotificationContainer, scenes: Set<Scene>, Loading @@ -40,11 +39,13 @@ class SceneWindowRootView( sceneDataSourceDelegator: SceneDataSourceDelegator, alternateBouncerDependencies: AlternateBouncerDependencies, ) { this.viewModel = viewModel setLayoutInsetsController(layoutInsetController) SceneWindowRootViewBinder.bind( view = this@SceneWindowRootView, viewModel = viewModel, viewModelFactory = viewModelFactory, motionEventHandlerReceiver = { motionEventHandler -> this.motionEventHandler = motionEventHandler }, windowInsets = windowInsets, containerConfig = containerConfig, sharedNotificationContainer = sharedNotificationContainer, Loading @@ -69,10 +70,10 @@ class SceneWindowRootView( } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { viewModel.onMotionEvent(ev) motionEventHandler?.onMotionEvent(ev) return super.dispatchTouchEvent(ev).also { TouchLogger.logDispatchTouch(TAG, ev, it) viewModel.onMotionEventComplete() motionEventHandler?.onMotionEventComplete() } } Loading
packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +14 −8 Original line number Diff line number Diff line Loading @@ -29,8 +29,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.animation.scene.SceneKey import com.android.compose.theme.PlatformTheme import com.android.internal.policy.ScreenDecorationsUtils Loading @@ -39,7 +37,9 @@ import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider import com.android.systemui.keyguard.ui.composable.AlternateBouncer import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.viewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scene Loading @@ -51,6 +51,7 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map Loading @@ -63,7 +64,8 @@ object SceneWindowRootViewBinder { /** Binds between the view and view-model pertaining to a specific scene container. */ fun bind( view: ViewGroup, viewModel: SceneContainerViewModel, viewModelFactory: SceneContainerViewModel.Factory, motionEventHandlerReceiver: (SceneContainerViewModel.MotionEventHandler?) -> Unit, windowInsets: StateFlow<WindowInsets?>, containerConfig: SceneContainerConfig, sharedNotificationContainer: SharedNotificationContainer, Loading @@ -85,8 +87,11 @@ object SceneWindowRootViewBinder { } view.repeatWhenAttached { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { view.viewModel( minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModelFactory.create(motionEventHandlerReceiver) }, ) { viewModel -> try { view.setViewTreeOnBackPressedDispatcherOwner( object : OnBackPressedDispatcherOwner { override val onBackPressedDispatcher = Loading Loading @@ -140,13 +145,14 @@ object SceneWindowRootViewBinder { onVisibilityChangedInternal(isVisible) } } } awaitCancellation() } finally { // Here when destroyed. view.removeAllViews() } } } } private fun createSceneContainerView( scope: CoroutineScope, Loading
packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +48 −23 Original line number Diff line number Diff line Loading @@ -23,25 +23,26 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import javax.inject.Inject import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map /** Models UI state for the scene container. */ @SysUISingleton class SceneContainerViewModel @Inject @AssistedInject constructor( private val sceneInteractor: SceneInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, ) { @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : SysUiViewModel() { /** * Keys of all scenes in the container. * Loading @@ -56,6 +57,29 @@ constructor( /** Whether the container is visible. */ val isVisible: StateFlow<Boolean> = sceneInteractor.isVisible override suspend fun onActivated() { try { // Sends a MotionEventHandler to the owner of the view-model so they can report // MotionEvents into the view-model. motionEventHandlerReceiver( object : MotionEventHandler { override fun onMotionEvent(motionEvent: MotionEvent) { this@SceneContainerViewModel.onMotionEvent(motionEvent) } override fun onMotionEventComplete() { this@SceneContainerViewModel.onMotionEventComplete() } } ) awaitCancellation() } finally { // Clears the previously-sent MotionEventHandler so the owner of the view-model releases // their reference to it. motionEventHandlerReceiver(null) } } /** * Binds the given flow so the system remembers it. * Loading Loading @@ -136,21 +160,22 @@ constructor( } } private fun replaceSceneFamilies( destinationScenes: Map<UserAction, UserActionResult>, ): Flow<Map<UserAction, UserActionResult>> { return destinationScenes .mapValues { (_, actionResult) -> sceneInteractor.resolveSceneFamily(actionResult.toScene).map { scene -> actionResult.copy(toScene = scene) } /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */ interface MotionEventHandler { /** Notifies that a [MotionEvent] has occurred. */ fun onMotionEvent(motionEvent: MotionEvent) /** * Notifies that the previous [MotionEvent] reported by [onMotionEvent] has finished * processing. */ fun onMotionEventComplete() } .combineValueFlows() @AssistedFactory interface Factory { fun create( motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ): SceneContainerViewModel } } private fun <K, V> Map<K, Flow<V>>.combineValueFlows(): Flow<Map<K, V>> = combine( asIterable().map { (k, fv) -> fv.map { k to it } }, transform = Array<Pair<K, V>>::toMap, )