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

Commit 5ad010b4 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Interrupted predictive back does not call canChangeScene

This CL ensures that STLState.canChangeScene() is only called right
before the actual STLState.transitionState.currentScene is going to be
changed. In particular, it should not be called if the predictive back
transition is interrupted by another transition.

Bug: 350705972
Test: PredictiveBackHandlerTest
Flag: com.android.systemui.scene_container
Change-Id: I79fd72609d78982b17caa5303154d4e40e3d7ae4
parent 15fe59f3
Loading
Loading
Loading
Loading
+7 −10
Original line number Diff line number Diff line
@@ -56,13 +56,7 @@ internal fun PredictiveBackHandler(
            progress.collect { backEvent -> transition.dragProgress = backEvent.progress }

            // Back gesture successful.
            transition.animateTo(
                if (state.canChangeScene(targetSceneForBack)) {
                    targetSceneForBack
                } else {
                    fromScene
                }
            )
            transition.animateTo(targetSceneForBack)
        } catch (e: CancellationException) {
            // Back gesture cancelled.
            transition.animateTo(fromScene)
@@ -105,12 +99,15 @@ private class PredictiveBackTransition(
            return it
        }

        if (scene != currentScene && state.transitionState == this && state.canChangeScene(scene)) {
            currentScene = scene
        }

        val targetProgress =
            when (scene) {
            when (currentScene) {
                fromScene -> 0f
                toScene -> 1f
                else -> error("scene $scene should be either $fromScene or $toScene")
                else -> error("scene $currentScene should be either $fromScene or $toScene")
            }

        val animatable = Animatable(dragProgress).also { progressAnimatable = it }
+54 −0
Original line number Diff line number Diff line
@@ -20,12 +20,16 @@ import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.subjects.assertThat
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -94,6 +98,56 @@ class PredictiveBackHandlerTest {
        assertThat(layoutState.transitionState).isIdle()
    }

    @Test
    fun interruptedPredictiveBackDoesNotCallCanChangeScene() {
        var canChangeSceneCalled = false
        val layoutState =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutState(
                    SceneA,
                    canChangeScene = {
                        canChangeSceneCalled = true
                        true
                    },
                )
            }

        lateinit var coroutineScope: CoroutineScope
        rule.setContent {
            coroutineScope = rememberCoroutineScope()
            SceneTransitionLayout(layoutState) {
                scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
                scene(SceneB) { Box(Modifier.fillMaxSize()) }
                scene(SceneC) { Box(Modifier.fillMaxSize()) }
            }
        }

        assertThat(layoutState.transitionState).hasCurrentScene(SceneA)

        // Start back.
        val dispatcher = rule.activity.onBackPressedDispatcher
        rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) }

        val predictiveTransition = assertThat(layoutState.transitionState).isTransition()
        assertThat(predictiveTransition).hasFromScene(SceneA)
        assertThat(predictiveTransition).hasToScene(SceneB)

        // Start a new transition to C.
        rule.runOnUiThread { layoutState.setTargetScene(SceneC, coroutineScope) }
        val newTransition = assertThat(layoutState.transitionState).isTransition()
        assertThat(newTransition).hasFromScene(SceneA)
        assertThat(newTransition).hasToScene(SceneC)

        // Commit the back gesture. It shouldn't call canChangeScene given that the back transition
        // was interrupted.
        rule.runOnUiThread { dispatcher.onBackPressed() }
        rule.waitForIdle()
        assertThat(layoutState.transitionState).hasCurrentScene(SceneC)
        assertThat(layoutState.transitionState).isIdle()
        assertThat(predictiveTransition).hasCurrentScene(SceneA)
        assertThat(canChangeSceneCalled).isFalse()
    }

    private fun backEvent(progress: Float = 0f): BackEventCompat {
        return BackEventCompat(
            touchX = 0f,