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

Commit d142cf11 authored by Coco Duan's avatar Coco Duan
Browse files

Fix flickering when swiping back to keyguard from hub in landscape

The keyguard elements sometimes flicker when exiting hub in landscape,
if the transition animation starts early, before screen rotation.

The fix is to monitor orientation changes from the Communal Container
and add two flows that derives from the orientation change:
a flow for landscape-to-portrait transition,
a flow for screen rotated to portrait,
to help the Communal Container handle landscape swipe and the Transition
ViewModel control keyguard animation.

Fixes: b/400464568
Test: atest GlanceableHubToLockscreenTransitionViewModelTest
Test: atest CommunalViewModelTest
Flag: com.android.systemui.glanceable_hub_v2
Change-Id: I74c8305131f2a63ba96c132b83884c7057a6f87a
parent eaab750a
Loading
Loading
Loading
Loading
+42 −1
Original line number Diff line number Diff line
package com.android.systemui.communal.ui.compose

import android.content.res.Configuration
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -26,6 +28,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.disabled
@@ -54,6 +57,7 @@ import com.android.systemui.communal.ui.compose.Dimensions.Companion.SlideOffset
import com.android.systemui.communal.ui.compose.extensions.allowGestures
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource
@@ -97,6 +101,17 @@ val sceneTransitionsV2 = transitions {
        spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS))
        fade(AllElements)
    }
    to(CommunalScenes.Blank, key = CommunalTransitionKeys.SwipeInLandscape) {
        spec = tween(durationMillis = TO_LOCKSCREEN_DURATION.toInt(DurationUnit.MILLISECONDS))
        translate(Communal.Elements.Grid, Edge.End)
        timestampRange(endMillis = 167) {
            fade(Communal.Elements.Grid)
            fade(Communal.Elements.IndicationArea)
            fade(Communal.Elements.LockIcon)
            fade(Communal.Elements.StatusBar)
        }
        timestampRange(startMillis = 167, endMillis = 500) { fade(Communal.Elements.Scrim) }
    }
    to(CommunalScenes.Blank, key = CommunalTransitionKeys.Swipe) {
        spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS)
        translate(Communal.Elements.Grid, Edge.End)
@@ -209,6 +224,9 @@ fun CommunalContainer(

    val blurRadius = with(LocalDensity.current) { viewModel.blurRadiusPx.toDp() }

    val swipeFromHubInLandscape by
        viewModel.swipeFromHubInLandscape.collectAsStateWithLifecycle(false)

    SceneTransitionLayout(
        state = state,
        modifier = modifier.fillMaxSize().thenIf(isUiBlurred) { Modifier.blur(blurRadius) },
@@ -236,7 +254,14 @@ fun CommunalContainer(
            userActions =
                mapOf(
                    Swipe.End to
                        UserActionResult(CommunalScenes.Blank, CommunalTransitionKeys.Swipe)
                        UserActionResult(
                            CommunalScenes.Blank,
                            if (swipeFromHubInLandscape) {
                                CommunalTransitionKeys.SwipeInLandscape
                            } else {
                                CommunalTransitionKeys.Swipe
                            },
                        )
                ),
        ) {
            CommunalScene(
@@ -253,6 +278,20 @@ fun CommunalContainer(
    Box(modifier = Modifier.fillMaxSize().allowGestures(touchesAllowed))
}

/** Listens to orientation changes on communal scene and reset when scene is disposed. */
@Composable
fun ObserveOrientationChange(viewModel: CommunalViewModel) {
    val configuration = LocalConfiguration.current

    LaunchedEffect(configuration.orientation) {
        viewModel.onOrientationChange(configuration.orientation)
    }

    DisposableEffect(Unit) {
        onDispose { viewModel.onOrientationChange(Configuration.ORIENTATION_UNDEFINED) }
    }
}

/** Scene containing the glanceable hub UI. */
@Composable
fun ContentScope.CommunalScene(
@@ -264,6 +303,8 @@ fun ContentScope.CommunalScene(
) {
    val isFocusable by viewModel.isFocusable.collectAsStateWithLifecycle(initialValue = false)

    // Observe screen rotation while Communal Scene is active.
    ObserveOrientationChange(viewModel)
    Box(
        modifier =
            Modifier.element(Communal.Elements.Scrim)
+62 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.communal.domain.interactor

import android.content.res.Configuration.ORIENTATION_LANDSCAPE
import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
@@ -33,11 +35,15 @@ import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.initialSceneKey
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,9 +52,11 @@ import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@@ -70,6 +78,7 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase(

    private val repository = kosmos.communalSceneRepository
    private val underTest by lazy { kosmos.communalSceneInteractor }
    private val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController

    @DisableFlags(FLAG_SCENE_CONTAINER)
    @Test
@@ -551,4 +560,57 @@ class CommunalSceneInteractorTest(flags: FlagsParameterization) : SysuiTestCase(
            transitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
            assertThat(isCommunalVisible).isEqualTo(false)
        }

    @Test
    fun willRotateToPortrait_whenKeyguardRotationNotAllowed() =
        testScope.runTest {
            whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(false)
            val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait)

            repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE)
            runCurrent()

            assertThat(willRotateToPortrait).isEqualTo(true)

            repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
            runCurrent()

            assertThat(willRotateToPortrait).isEqualTo(false)
        }

    @Test
    fun willRotateToPortrait_isFalse_whenKeyguardRotationIsAllowed() =
        testScope.runTest {
            whenever(keyguardStateController.isKeyguardScreenRotationAllowed()).thenReturn(true)
            val willRotateToPortrait by collectLastValue(underTest.willRotateToPortrait)

            repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE)
            runCurrent()

            assertThat(willRotateToPortrait).isEqualTo(false)

            repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
            runCurrent()

            assertThat(willRotateToPortrait).isEqualTo(false)
        }

    @Test
    fun rotatedToPortrait() =
        testScope.runTest {
            val rotatedToPortrait by collectLastValue(underTest.rotatedToPortrait)
            assertThat(rotatedToPortrait).isEqualTo(false)

            repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
            runCurrent()
            assertThat(rotatedToPortrait).isEqualTo(false)

            repository.setCommunalContainerOrientation(ORIENTATION_LANDSCAPE)
            runCurrent()
            assertThat(rotatedToPortrait).isEqualTo(false)

            repository.setCommunalContainerOrientation(ORIENTATION_PORTRAIT)
            runCurrent()
            assertThat(rotatedToPortrait).isEqualTo(true)
        }
}
+225 −4
Original line number Diff line number Diff line
@@ -17,11 +17,20 @@
package com.android.systemui.keyguard.ui.viewmodel

import android.content.res.Configuration
import android.content.res.mainResources
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.util.LayoutDirection
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -29,33 +38,166 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.transitions.blurConfig
import com.android.systemui.kosmos.collectValues
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@RunWith(AndroidJUnit4::class)
class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
    val kosmos = testKosmos()
@RunWith(ParameterizedAndroidJunit4::class)
class GlanceableHubToLockscreenTransitionViewModelTest(flags: FlagsParameterization) :
    SysuiTestCase() {
    val kosmos = testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources }
    val testScope = kosmos.testScope

    val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
    val configurationRepository = kosmos.fakeConfigurationRepository
    val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController
    val underTest by lazy { kosmos.glanceableHubToLockscreenTransitionViewModel }

    companion object {
        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams(): List<FlagsParameterization> {
            return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2)
        }
    }

    init {
        mSetFlagsRule.setFlagsParameterization(flags)
    }

    @Test
    fun lockscreenFadeIn() =
        kosmos.runTest {
            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")

            val values by collectValues(underTest.keyguardAlpha)
            assertThat(values).isEmpty()

            keyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0f, TransitionState.STARTED),
                    // Should start running here...
                    step(0.1f),
                    step(0.2f),
                    step(0.3f),
                    step(0.4f),
                    // ...up to here
                    step(0.5f),
                    step(0.6f),
                    step(0.7f),
                    step(0.8f),
                    step(1f),
                ),
                testScope,
            )

            assertThat(values).hasSize(4)
            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
        }

    @Test
    @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
    fun lockscreenFadeIn_fromHubInLandscape() =
        kosmos.runTest {
            kosmos.setCommunalV2ConfigEnabled(true)
            whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
            communalSceneRepository.setCommunalContainerOrientation(
                Configuration.ORIENTATION_LANDSCAPE
            )

            val values by collectValues(underTest.keyguardAlpha)
            assertThat(values).isEmpty()

            // Exit hub to lockscreen
            val progress = MutableStateFlow(0f)
            val transitionState =
                MutableStateFlow(
                    ObservableTransitionState.Transition(
                        fromScene = CommunalScenes.Communal,
                        toScene = CommunalScenes.Blank,
                        currentScene = flowOf(CommunalScenes.Blank),
                        progress = progress,
                        isInitiatedByUserInput = false,
                        isUserInputOngoing = flowOf(false),
                    )
                )
            communalSceneInteractor.setTransitionState(transitionState)
            progress.value = .2f

            // Still in landscape
            keyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0f, TransitionState.STARTED),
                    step(0.1f),
                    // start here..
                    step(0.5f),
                ),
                testScope,
            )

            // Communal container is rotated to portrait
            communalSceneRepository.setCommunalContainerOrientation(
                Configuration.ORIENTATION_PORTRAIT
            )
            runCurrent()

            keyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0.6f),
                    step(0.7f),
                    // should stop here..
                    step(0.8f),
                    step(1f),
                ),
                testScope,
            )
            // Scene transition finished.
            progress.value = 1f
            keyguardTransitionRepository.sendTransitionSteps(
                listOf(step(1f, TransitionState.FINISHED)),
                testScope,
            )

            assertThat(values).hasSize(4)
            // onStart
            assertThat(values[0]).isEqualTo(0f)
            assertThat(values[1]).isEqualTo(0f)
            assertThat(values[2]).isEqualTo(1f)
            // onFinish
            assertThat(values[3]).isEqualTo(1f)
        }

    @Test
    @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
    fun lockscreenFadeIn_v2FlagDisabledAndFromHubInLandscape() =
        kosmos.runTest {
            whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)
            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
            // Rotation is not enabled so communal container is in portrait.
            communalSceneRepository.setCommunalContainerOrientation(
                Configuration.ORIENTATION_PORTRAIT
            )

            val values by collectValues(underTest.keyguardAlpha)
            assertThat(values).isEmpty()

            // Exit hub to lockscreen
            keyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0f, TransitionState.STARTED),
@@ -89,6 +231,8 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
                R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
                100,
            )
            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")

            val values by collectValues(underTest.keyguardTranslationX)
            assertThat(values).isEmpty()

@@ -107,6 +251,44 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
            values.forEach { assertThat(it.value).isIn(Range.closed(-100f, 0f)) }
        }

    @Test
    @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
    fun lockscreenTranslationX_fromHubInLandscape() =
        kosmos.runTest {
            kosmos.setCommunalV2ConfigEnabled(true)
            val config: Configuration = mock()
            whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR)
            configurationRepository.onConfigurationChange(config)

            configurationRepository.setDimensionPixelSize(
                R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
                100,
            )
            whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)

            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
            communalSceneRepository.setCommunalContainerOrientation(
                Configuration.ORIENTATION_LANDSCAPE
            )

            val values by collectValues(underTest.keyguardTranslationX)
            assertThat(values).isEmpty()

            keyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0f, TransitionState.STARTED),
                    step(0.3f),
                    step(0.5f),
                    step(0.7f),
                    step(1f),
                    step(1f, TransitionState.FINISHED),
                ),
                testScope,
            )
            // no translation-x animation
            values.forEach { assertThat(it.value).isEqualTo(0f) }
        }

    @Test
    fun lockscreenTranslationX_resetsAfterCancellation() =
        kosmos.runTest {
@@ -118,6 +300,9 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
                R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
                100,
            )

            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")

            val values by collectValues(underTest.keyguardTranslationX)
            assertThat(values).isEmpty()

@@ -136,6 +321,42 @@ class GlanceableHubToLockscreenTransitionViewModelTest : SysuiTestCase() {
            assertThat(values.last().value).isEqualTo(0f)
        }

    @Test
    @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
    fun lockscreenTranslationX_resetsAfterCancellation_fromHubInLandscape() =
        kosmos.runTest {
            kosmos.setCommunalV2ConfigEnabled(true)
            val config: Configuration = mock()
            whenever(config.layoutDirection).thenReturn(LayoutDirection.LTR)
            configurationRepository.onConfigurationChange(config)

            configurationRepository.setDimensionPixelSize(
                R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x,
                100,
            )
            whenever(keyguardStateController.isKeyguardScreenRotationAllowed).thenReturn(false)

            communalSceneInteractor.changeScene(CommunalScenes.Communal, "test")
            communalSceneRepository.setCommunalContainerOrientation(
                Configuration.ORIENTATION_LANDSCAPE
            )

            val values by collectValues(underTest.keyguardTranslationX)
            assertThat(values).isEmpty()

            keyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0f, TransitionState.STARTED),
                    step(0.3f),
                    step(0.6f),
                    step(0.9f, TransitionState.CANCELED),
                ),
                testScope,
            )
            // no translation-x animation
            values.forEach { assertThat(it.value).isEqualTo(0f) }
        }

    @Test
    @DisableSceneContainer
    fun blurBecomesMinValueImmediately() =
+92 −1

File changed.

Preview size limit exceeded, changes collapsed.

+16 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal.data.repository

import android.content.res.Configuration
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
@@ -49,6 +50,9 @@ interface CommunalSceneRepository {
    /** Exposes the transition state of the communal [SceneTransitionLayout]. */
    val transitionState: StateFlow<ObservableTransitionState>

    /** Current orientation of the communal container. */
    val communalContainerOrientation: StateFlow<Int>

    /** Updates the requested scene. */
    fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null)

@@ -64,6 +68,9 @@ interface CommunalSceneRepository {
     * Note that you must call is with `null` when the UI is done or risk a memory leak.
     */
    fun setTransitionState(transitionState: Flow<ObservableTransitionState>?)

    /** Set the current orientation of the communal container. */
    fun setCommunalContainerOrientation(orientation: Int)
}

@SysUISingleton
@@ -89,6 +96,11 @@ constructor(
                initialValue = defaultTransitionState,
            )

    private val _communalContainerOrientation =
        MutableStateFlow(Configuration.ORIENTATION_UNDEFINED)
    override val communalContainerOrientation: StateFlow<Int> =
        _communalContainerOrientation.asStateFlow()

    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
        applicationScope.launch {
            // SceneTransitionLayout state updates must be triggered on the thread the STL was
@@ -105,6 +117,10 @@ constructor(
        }
    }

    override fun setCommunalContainerOrientation(orientation: Int) {
        _communalContainerOrientation.value = orientation
    }

    override suspend fun showHubFromPowerButton() {
        // If keyguard is not showing yet, the hub view is not ready and the
        // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource]
Loading