Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 649c790a authored by Andreas Miko's avatar Andreas Miko
Browse files

Fix wildcard edges by simulating TransitionSteps in KTI

Wildcard edges are broken because they miss events in STL which in the
past would have been KTF transitions. On closer inspection wildcard
edges with a KTF node are not broken because all transitions leading to
that edge will necessarily emit a KTF transition, even when its coming
from the outside (UNDEFINED). The same can't be said for wildcard edges
with STL nodes.

A transition() consumer passing Edge(from=Scenes.Gone) will receive
transition updates for AOD -> Scenes.Gone because KTF is involved but it
 will be completely unaware of Scenes.Bouncer -> Scenes.Gone which is
 both inconsistent and wrong.

This CL fixes this by constructing a flow of simulated TransitionSteps
based on STLs transition state (they never actually appear within KTF)
and returns it to only this specific consumers that pass a wildcard edge
 with a scene.

Bug: 330311871
Flag: com.android.systemui.scene_container
Test: added unit tests
Change-Id: I35bc1b0290108efc2280086b65870086418080a1
parent 7792e57f
Loading
Loading
Loading
Loading
+222 −592

File changed.

Preview size limit exceeded, changes collapsed.

+153 −34
Original line number Original line Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor


import android.annotation.SuppressLint
import android.annotation.SuppressLint
import android.util.Log
import android.util.Log
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Application
@@ -37,15 +38,22 @@ import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.onStart
@@ -206,8 +214,13 @@ constructor(
                )
                )
            }
            }


        return if (SceneContainerFlag.isEnabled) {
        if (!SceneContainerFlag.isEnabled) {
            flow.filter { step ->
            return flow
        }
        if (edge.isSceneWildcardEdge()) {
            return simulateTransitionStepsForSceneTransitions(edge)
        }
        return flow.filter { step ->
            val fromScene =
            val fromScene =
                when (edge) {
                when (edge) {
                    is Edge.StateToState -> edge.from?.mapToSceneContainerScene()
                    is Edge.StateToState -> edge.from?.mapToSceneContainerScene()
@@ -222,8 +235,6 @@ constructor(
                    is Edge.SceneToState -> edge.to?.mapToSceneContainerScene()
                    is Edge.SceneToState -> edge.to?.mapToSceneContainerScene()
                }
                }


                fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null

            val isTransitioningBetweenLockscreenStates =
            val isTransitioningBetweenLockscreenStates =
                fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull()
                fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull()
            val isTransitioningBetweenDesiredScenes =
            val isTransitioningBetweenDesiredScenes =
@@ -242,8 +253,116 @@ constructor(
                isTransitioningBetweenDesiredScenes ||
                isTransitioningBetweenDesiredScenes ||
                terminalStepBelongsToPreviousTransition
                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
                        }
                    }
                }
            }
        }
        }
    }
    }