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

Commit 972694be authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Fix wildcard edges by simulating TransitionSteps in KTI" into main

parents c23e6049 649c790a
Loading
Loading
Loading
Loading
+222 −592

File changed.

Preview size limit exceeded, changes collapsed.

+153 −34
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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()
@@ -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 =
@@ -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
                        }
                    }
                }
            }
        }
    }