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

Commit 60eef3c4 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Use transitionState instead of currentScene.

There's often a mismatch between the currentScene and the UI because
currentScene drives the UI (when programmatic scene changes are
requested) but also the UI updates currentScene (when a
user action completes).

This CL simplifies the API surface such that external consumers of the
Flexiglass API can use the transitionState, driven directly from the UI
to know about transition pairs and progress of transitions (vs. idle).

Unfortunately, I did have to keep currentScene (renamed to
"desiredScene" to better capture its role in the system) so it can be
served downstream to the UI for servicing programmatic scene changes.

In addition, many of the existing consumers of Flexiglass were updated
to no longer rely on currentScene as it would (a) often mismatch what
the UI is showing and (b) not provide enough context to know about the
state of transitions.

Fix: 294220005
Test: Unit tests updated
Test: Manually verified with auth method None, Swipe, and Pattern - on,
unlock, lock, swipe down to reveal shade and QS, swipe up to reveal
bouncer, back gesture to return to lockscreen.

Change-Id: I3de051f7c0331f39679c15ec0c099cd574b68935
parent a5ed8c10
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ fun SceneContainer(

    SceneTransitionLayout(
        currentScene = currentSceneKey.toTransitionSceneKey(),
        onChangeScene = { sceneKey -> viewModel.setCurrentScene(sceneKey.toModel()) },
        onChangeScene = viewModel::onSceneChanged,
        transitions = transitions {},
        state = state,
        modifier = modifier.fillMaxSize(),
@@ -154,3 +154,7 @@ private fun UserAction.toTransitionUserAction(): SceneTransitionUserAction {
        is UserAction.Back -> Back
    }
}

private fun SceneContainerViewModel.onSceneChanged(sceneKey: SceneTransitionSceneKey) {
    onSceneChanged(sceneKey.toModel())
}
+10 −12
Original line number Diff line number Diff line
@@ -476,18 +476,16 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
        if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
            // When the scene framework transitions from bouncer to gone, we dismiss the keyguard.
            mSceneTransitionCollectionJob = mJavaAdapter.get().alwaysCollectFlow(
                mSceneInteractor.get().getTransitions(),
                sceneTransitionModel -> {
                    if (sceneTransitionModel != null
                            && sceneTransitionModel.getFrom() == SceneKey.Bouncer.INSTANCE
                            && sceneTransitionModel.getTo() == SceneKey.Gone.INSTANCE) {
                mSceneInteractor.get().finishedSceneTransitions(
                    /* from= */ SceneKey.Bouncer.INSTANCE,
                    /* to= */ SceneKey.Gone.INSTANCE),
                unused -> {
                    final int selectedUserId = mUserInteractor.getSelectedUserId();
                    showNextSecurityScreenOrFinish(
                            /* authenticated= */ true,
                            selectedUserId,
                            /* bypassSecondaryLockScreen= */ true,
                            mSecurityModel.getSecurityMode(selectedUserId));
                    }
                });
        }
    }
+3 −3
Original line number Diff line number Diff line
@@ -116,12 +116,12 @@ constructor(
                repository.setMessage(
                    message ?: promptMessage(authenticationInteractor.getAuthenticationMethod())
                )
                sceneInteractor.setCurrentScene(
                sceneInteractor.changeScene(
                    scene = SceneModel(SceneKey.Bouncer),
                    loggingReason = "request to unlock device while authentication required",
                )
            } else {
                sceneInteractor.setCurrentScene(
                sceneInteractor.changeScene(
                    scene = SceneModel(SceneKey.Gone),
                    loggingReason = "request to unlock device while authentication isn't required",
                )
@@ -169,7 +169,7 @@ constructor(
            authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null

        if (isAuthenticated) {
            sceneInteractor.setCurrentScene(
            sceneInteractor.changeScene(
                scene = SceneModel(SceneKey.Gone),
                loggingReason = "successful authentication",
            )
+24 −46
Original line number Diff line number Diff line
@@ -18,50 +18,49 @@

package com.android.systemui.scene.data.repository

import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.SceneTransitionModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn

/** Source of truth for scene framework application state. */
class SceneContainerRepository
@Inject
constructor(
    @Application applicationScope: CoroutineScope,
    private val config: SceneContainerConfig,
) {
    private val _desiredScene = MutableStateFlow(SceneModel(config.initialSceneKey))
    val desiredScene: StateFlow<SceneModel> = _desiredScene.asStateFlow()

    private val _isVisible = MutableStateFlow(true)
    val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow()

    private val _currentScene = MutableStateFlow(SceneModel(config.initialSceneKey))
    val currentScene: StateFlow<SceneModel> = _currentScene.asStateFlow()

    private val transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null)
    val transitionProgress: Flow<Float> =
        transitionState.flatMapLatest { observableTransitionStateFlow ->
            observableTransitionStateFlow?.flatMapLatest { observableTransitionState ->
                when (observableTransitionState) {
                    is ObservableTransitionState.Idle -> flowOf(1f)
                    is ObservableTransitionState.Transition -> observableTransitionState.progress
                }
            }
                ?: flowOf(1f)
        }

    private val _transitions = MutableStateFlow<SceneTransitionModel?>(null)
    val transitions: StateFlow<SceneTransitionModel?> = _transitions.asStateFlow()
    private val defaultTransitionState = ObservableTransitionState.Idle(config.initialSceneKey)
    private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null)
    val transitionState: StateFlow<ObservableTransitionState> =
        _transitionState
            .flatMapLatest { innerFlowOrNull -> innerFlowOrNull ?: flowOf(defaultTransitionState) }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                initialValue = defaultTransitionState,
            )

    /**
     * Returns the keys to all scenes in the container with the given name.
     * Returns the keys to all scenes in the container.
     *
     * The scenes will be sorted in z-order such that the last one is the one that should be
     * rendered on top of all previous ones.
@@ -70,40 +69,19 @@ constructor(
        return config.sceneKeys
    }

    /** Sets the current scene in the container with the given name. */
    fun setCurrentScene(scene: SceneModel) {
    fun setDesiredScene(scene: SceneModel) {
        check(allSceneKeys().contains(scene.key)) {
            """
                Cannot set current scene key to "${scene.key}". The configuration does not contain a
                scene with that key.
            """
                .trimIndent()
        }

        _currentScene.value = scene
    }

    /** Sets the scene transition in the container with the given name. */
    fun setSceneTransition(from: SceneKey, to: SceneKey) {
        check(allSceneKeys().contains(from)) {
            """
                Cannot set current scene key to "$from". The configuration does not contain a scene
                with that key.
            """
                .trimIndent()
        }
        check(allSceneKeys().contains(to)) {
            """
                Cannot set current scene key to "$to". The configuration does not contain a scene
                with that key.
                Cannot set the desired scene key to "${scene.key}". The configuration does not
                contain a scene with that key.
            """
                .trimIndent()
        }

        _transitions.value = SceneTransitionModel(from = from, to = to)
        _desiredScene.value = scene
    }

    /** Sets whether the container with the given name is visible. */
    /** Sets whether the container is visible. */
    fun setVisible(isVisible: Boolean) {
        _isVisible.value = isVisible
    }
@@ -114,6 +92,6 @@ constructor(
     * Note that you must call is with `null` when the UI is done or risk a memory leak.
     */
    fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
        this.transitionState.value = transitionState
        _transitionState.value = transitionState
    }
}
+106 −35
Original line number Diff line number Diff line
@@ -23,12 +23,15 @@ import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.SceneTransitionModel
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull

/**
 * Generic business logic and app state accessors for the scene framework.
@@ -46,7 +49,54 @@ constructor(
) {

    /**
     * Returns the keys of all scenes in the container with the given name.
     * The currently *desired* scene.
     *
     * **Important:** this value will _commonly be different_ from what is being rendered in the UI,
     * by design.
     *
     * There are two intended sources for this value:
     * 1. Programmatic requests to transition to another scene (calls to [changeScene]).
     * 2. Reports from the UI about completing a transition to another scene (calls to
     *    [onSceneChanged]).
     *
     * Both the sources above cause the value of this flow to change; however, they cause mismatches
     * in different ways.
     *
     * **Updates from programmatic transitions**
     *
     * When an external bit of code asks the framework to switch to another scene, the value here
     * will update immediately. Downstream, the UI will detect this change and initiate the
     * transition animation. As the transition animation progresses, a threshold will be reached, at
     * which point the UI and the state here will match each other.
     *
     * **Updates from the UI**
     *
     * When the user interacts with the UI, the UI runs a transition animation that tracks the user
     * pointer (for example, the user's finger). During this time, the state value here and what the
     * UI shows will likely not match. Once/if a threshold is met, the UI reports it and commits the
     * change, making the value here match the UI again.
     */
    val desiredScene: StateFlow<SceneModel> = repository.desiredScene

    /**
     * The current state of the transition.
     *
     * Consumers should use this state to know:
     * 1. Whether there is an ongoing transition or if the system is at rest.
     * 2. When transitioning, which scenes are being transitioned between.
     * 3. When transitioning, what the progress of the transition is.
     */
    val transitionState: StateFlow<ObservableTransitionState> = repository.transitionState

    /** Whether the scene container is visible. */
    val isVisible: StateFlow<Boolean> = repository.isVisible

    private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null)
    /** A flow of motion events originating from outside of the scene framework. */
    val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow()

    /**
     * Returns the keys of all scenes in the container.
     *
     * The scenes will be sorted in z-order such that the last one is the one that should be
     * rendered on top of all previous ones.
@@ -55,26 +105,20 @@ constructor(
        return repository.allSceneKeys()
    }

    /** Sets the scene in the container with the given name. */
    fun setCurrentScene(scene: SceneModel, loggingReason: String) {
        val currentSceneKey = repository.currentScene.value.key
        if (currentSceneKey == scene.key) {
            return
        }

        logger.logSceneChange(
            from = currentSceneKey,
            to = scene.key,
            reason = loggingReason,
        )
        repository.setCurrentScene(scene)
        repository.setSceneTransition(from = currentSceneKey, to = scene.key)
    /**
     * Requests a scene change to the given scene.
     *
     * The change is animated. Therefore, while the value in [desiredScene] will update immediately,
     * it will be some time before the UI will switch to the desired scene. The scene change
     * requested is remembered here but served by the UI layer, which will start a transition
     * animation. Once enough of the transition has occurred, the system will come into agreement
     * between the [desiredScene] and the UI.
     */
    fun changeScene(scene: SceneModel, loggingReason: String) {
        updateDesiredScene(scene, loggingReason, logger::logSceneChangeRequested)
    }

    /** The current scene in the container with the given name. */
    val currentScene: StateFlow<SceneModel> = repository.currentScene

    /** Sets the visibility of the container with the given name. */
    /** Sets the visibility of the container. */
    fun setVisible(isVisible: Boolean, loggingReason: String) {
        val wasVisible = repository.isVisible.value
        if (wasVisible == isVisible) {
@@ -89,9 +133,6 @@ constructor(
        return repository.setVisible(isVisible)
    }

    /** Whether the container with the given name is visible. */
    val isVisible: StateFlow<Boolean> = repository.isVisible

    /**
     * Binds the given flow so the system remembers it.
     *
@@ -101,23 +142,53 @@ constructor(
        repository.setTransitionState(transitionState)
    }

    /** Progress of the transition into the current scene in the container with the given name. */
    val transitionProgress: Flow<Float> = repository.transitionProgress

    /**
     * Scene transitions as pairs of keys. A new value is emitted exactly once, each time a scene
     * transition occurs. The flow begins with a `null` value at first, because the initial scene is
     * not something that we transition to from another scene.
     * Returns a stream of events that emits one [Unit] every time the framework transitions from
     * [from] to [to].
     */
    val transitions: StateFlow<SceneTransitionModel?> = repository.transitions

    private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null)

    /** A flow of motion events originating from outside of the scene framework. */
    val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow()
    fun finishedSceneTransitions(from: SceneKey, to: SceneKey): Flow<Unit> {
        return transitionState
            .mapNotNull { it as? ObservableTransitionState.Idle }
            .map { idleState -> idleState.scene }
            .distinctUntilChanged()
            .pairwise()
            .mapNotNull { (previousSceneKey, currentSceneKey) ->
                Unit.takeIf { previousSceneKey == from && currentSceneKey == to }
            }
    }

    /** Handles a remote user input. */
    fun onRemoteUserInput(input: RemoteUserInput) {
        _remoteUserInput.value = input
    }

    /**
     * Notifies that the UI has transitioned sufficiently to the given scene.
     *
     * *Not intended for external use!*
     *
     * Once a transition between one scene and another passes a threshold, the UI invokes this
     * method to report it, updating the value in [desiredScene] to match what the UI shows.
     */
    internal fun onSceneChanged(scene: SceneModel, loggingReason: String) {
        updateDesiredScene(scene, loggingReason, logger::logSceneChangeCommitted)
    }

    private fun updateDesiredScene(
        scene: SceneModel,
        loggingReason: String,
        log: (from: SceneKey, to: SceneKey, loggingReason: String) -> Unit,
    ) {
        val currentSceneKey = desiredScene.value.key
        if (currentSceneKey == scene.key) {
            return
        }

        log(
            /* from= */ currentSceneKey,
            /* to= */ scene.key,
            /* loggingReason= */ loggingReason,
        )
        repository.setDesiredScene(scene)
    }
}
Loading