Loading packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +222 −592 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +153 −34 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.annotation.SuppressLint import android.util.Log import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application Loading @@ -37,15 +38,22 @@ import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart Loading Loading @@ -206,8 +214,13 @@ constructor( ) } return if (SceneContainerFlag.isEnabled) { flow.filter { step -> if (!SceneContainerFlag.isEnabled) { return flow } if (edge.isSceneWildcardEdge()) { return simulateTransitionStepsForSceneTransitions(edge) } return flow.filter { step -> val fromScene = when (edge) { is Edge.StateToState -> edge.from?.mapToSceneContainerScene() Loading @@ -222,8 +235,6 @@ constructor( is Edge.SceneToState -> edge.to?.mapToSceneContainerScene() } fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null val isTransitioningBetweenLockscreenStates = fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull() val isTransitioningBetweenDesiredScenes = Loading @@ -242,8 +253,116 @@ constructor( isTransitioningBetweenDesiredScenes || terminalStepBelongsToPreviousTransition } } else { flow } private fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null /** * This function will return a flow that simulates TransitionSteps based on STL movements * filtered by [edge]. * * STL transitions outside of Lockscreen Transitions are not tracked in KTI. This is an issue * for wildcard edges, as this means that Scenes.Bouncer -> Scenes.Gone would not appear while * AOD -> Scenes.Bouncer would appear. * * This function will track STL transitions only when a wildcard edge is provided and emit a * RUNNING step for each update to [Transition.progress]. It will also emit a STARTED and * FINISHED step when the transitions starts and finishes. * * All TransitionSteps will have UNDEFINED as to and from state even when one of them is the * Lockscreen Scene. It indicates that both are scenes but it should not be relevant to * consumers of the [transition] API as usually all viewModels are just interested in the * progress value. The correct filtering based on the provided [edge] is always the * responsibility of KTI and therefore only proper [TransitionStep]s are emitted. The filter is * applied within this function. */ private fun simulateTransitionStepsForSceneTransitions(edge: Edge) = sceneInteractor.transitionState.flatMapLatestWithFinished { when (it) { is ObservableTransitionState.Idle -> { flowOf() } is ObservableTransitionState.Transition -> { val isMatchingTransition = when (edge) { is Edge.StateToState -> throw IllegalStateException("Should not be reachable.") is Edge.SceneToState -> it.isTransitioning(from = edge.from) is Edge.StateToScene -> it.isTransitioning(to = edge.to) } if (!isMatchingTransition) { return@flatMapLatestWithFinished flowOf() } flow { emit( TransitionStep( from = UNDEFINED, to = UNDEFINED, value = 0f, transitionState = TransitionState.STARTED, ) ) emitAll( it.progress.map { progress -> TransitionStep( from = UNDEFINED, to = UNDEFINED, value = progress, transitionState = TransitionState.RUNNING, ) } ) } } } } /** * This function is similar to flatMapLatest but it will additionally emit a FINISHED * TransitionStep whenever the flattened innerFlow emitted a STARTED step and is now being * replaced by a new innerFlow. * * This is to make sure that every STARTED step will receive a corresponding FINISHED step. * * We can't simply write this into a flow {} block because Transition.progress doesn't complete. * We also can't emit the FINISHED step simply when an Idle state is reached because a) * Transitions are not guaranteed to finish in Idle and b) There can be multiple Idle * transitions after another */ private fun <T> Flow<T>.flatMapLatestWithFinished( transform: suspend (T) -> Flow<TransitionStep> ): Flow<TransitionStep> = channelFlow { var job: Job? = null var startedEmitted = false coroutineScope { collect { value -> job?.cancelAndJoin() job = launch { val innerFlow = transform(value) try { innerFlow.collect { step -> if (step.transitionState == TransitionState.STARTED) { startedEmitted = true } send(step) } } finally { if (startedEmitted) { send( TransitionStep( from = UNDEFINED, to = UNDEFINED, value = 1f, transitionState = TransitionState.FINISHED, ) ) startedEmitted = false } } } } } } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +222 −592 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +153 −34 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.annotation.SuppressLint import android.util.Log import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application Loading @@ -37,15 +38,22 @@ import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart Loading Loading @@ -206,8 +214,13 @@ constructor( ) } return if (SceneContainerFlag.isEnabled) { flow.filter { step -> if (!SceneContainerFlag.isEnabled) { return flow } if (edge.isSceneWildcardEdge()) { return simulateTransitionStepsForSceneTransitions(edge) } return flow.filter { step -> val fromScene = when (edge) { is Edge.StateToState -> edge.from?.mapToSceneContainerScene() Loading @@ -222,8 +235,6 @@ constructor( is Edge.SceneToState -> edge.to?.mapToSceneContainerScene() } fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null val isTransitioningBetweenLockscreenStates = fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull() val isTransitioningBetweenDesiredScenes = Loading @@ -242,8 +253,116 @@ constructor( isTransitioningBetweenDesiredScenes || terminalStepBelongsToPreviousTransition } } else { flow } private fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null /** * This function will return a flow that simulates TransitionSteps based on STL movements * filtered by [edge]. * * STL transitions outside of Lockscreen Transitions are not tracked in KTI. This is an issue * for wildcard edges, as this means that Scenes.Bouncer -> Scenes.Gone would not appear while * AOD -> Scenes.Bouncer would appear. * * This function will track STL transitions only when a wildcard edge is provided and emit a * RUNNING step for each update to [Transition.progress]. It will also emit a STARTED and * FINISHED step when the transitions starts and finishes. * * All TransitionSteps will have UNDEFINED as to and from state even when one of them is the * Lockscreen Scene. It indicates that both are scenes but it should not be relevant to * consumers of the [transition] API as usually all viewModels are just interested in the * progress value. The correct filtering based on the provided [edge] is always the * responsibility of KTI and therefore only proper [TransitionStep]s are emitted. The filter is * applied within this function. */ private fun simulateTransitionStepsForSceneTransitions(edge: Edge) = sceneInteractor.transitionState.flatMapLatestWithFinished { when (it) { is ObservableTransitionState.Idle -> { flowOf() } is ObservableTransitionState.Transition -> { val isMatchingTransition = when (edge) { is Edge.StateToState -> throw IllegalStateException("Should not be reachable.") is Edge.SceneToState -> it.isTransitioning(from = edge.from) is Edge.StateToScene -> it.isTransitioning(to = edge.to) } if (!isMatchingTransition) { return@flatMapLatestWithFinished flowOf() } flow { emit( TransitionStep( from = UNDEFINED, to = UNDEFINED, value = 0f, transitionState = TransitionState.STARTED, ) ) emitAll( it.progress.map { progress -> TransitionStep( from = UNDEFINED, to = UNDEFINED, value = progress, transitionState = TransitionState.RUNNING, ) } ) } } } } /** * This function is similar to flatMapLatest but it will additionally emit a FINISHED * TransitionStep whenever the flattened innerFlow emitted a STARTED step and is now being * replaced by a new innerFlow. * * This is to make sure that every STARTED step will receive a corresponding FINISHED step. * * We can't simply write this into a flow {} block because Transition.progress doesn't complete. * We also can't emit the FINISHED step simply when an Idle state is reached because a) * Transitions are not guaranteed to finish in Idle and b) There can be multiple Idle * transitions after another */ private fun <T> Flow<T>.flatMapLatestWithFinished( transform: suspend (T) -> Flow<TransitionStep> ): Flow<TransitionStep> = channelFlow { var job: Job? = null var startedEmitted = false coroutineScope { collect { value -> job?.cancelAndJoin() job = launch { val innerFlow = transform(value) try { innerFlow.collect { step -> if (step.transitionState == TransitionState.STARTED) { startedEmitted = true } send(step) } } finally { if (startedEmitted) { send( TransitionStep( from = UNDEFINED, to = UNDEFINED, value = 1f, transitionState = TransitionState.FINISHED, ) ) startedEmitted = false } } } } } } Loading