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

Commit c60396c0 authored by William Xiao's avatar William Xiao
Browse files

Add keyguard transitions between lockscreen and glanceable hub

This change tracks transitions between the lockscreen and glanceable
hub based on the hub's exposed transition state. The hub's
transitions are the source of truth for directly controlling the
keyguard transition.

UI animations and more transitions will be in future CLs.

Bug: 315205222
Test: atest CommunalInteractorTest KeyguardTransitionScenariosTest
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT
Change-Id: I3328b2d238cacc2ee7b2738e05ebd72e5086acac
parent 257578d5
Loading
Loading
Loading
Loading
+131 −2
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -39,6 +40,9 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
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.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -69,9 +73,9 @@ class CommunalInteractorTest : SysuiTestCase() {
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        testScope = TestScope()
        testScope = TestScope(StandardTestDispatcher())

        val withDeps = CommunalInteractorFactory.create()
        val withDeps = CommunalInteractorFactory.create(testScope)

        tutorialRepository = withDeps.tutorialRepository
        communalRepository = withDeps.communalRepository
@@ -378,6 +382,131 @@ class CommunalInteractorTest : SysuiTestCase() {
            assertThat(desiredScene()).isEqualTo(targetScene)
        }

    @Test
    fun transitionProgress_onTargetScene_fullProgress() =
        testScope.runTest {
            val targetScene = CommunalSceneKey.Blank
            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
            val transitionProgress by collectLastValue(transitionProgressFlow)

            val transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(
                    ObservableCommunalTransitionState.Idle(targetScene)
                )
            underTest.setTransitionState(transitionState)

            // We're on the target scene.
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
        }

    @Test
    fun transitionProgress_notOnTargetScene_noProgress() =
        testScope.runTest {
            val targetScene = CommunalSceneKey.Blank
            val currentScene = CommunalSceneKey.Communal
            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
            val transitionProgress by collectLastValue(transitionProgressFlow)

            val transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(
                    ObservableCommunalTransitionState.Idle(currentScene)
                )
            underTest.setTransitionState(transitionState)

            // Transition progress is still idle, but we're not on the target scene.
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
        }

    @Test
    fun transitionProgress_transitioningToTrackedScene() =
        testScope.runTest {
            val currentScene = CommunalSceneKey.Communal
            val targetScene = CommunalSceneKey.Blank
            val transitionProgressFlow = underTest.transitionProgressToScene(targetScene)
            val transitionProgress by collectLastValue(transitionProgressFlow)

            var transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(
                    ObservableCommunalTransitionState.Idle(currentScene)
                )
            underTest.setTransitionState(transitionState)

            // Progress starts at 0.
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))

            val progress = MutableStateFlow(0f)
            transitionState =
                MutableStateFlow(
                    ObservableCommunalTransitionState.Transition(
                        fromScene = currentScene,
                        toScene = targetScene,
                        progress = progress,
                        isInitiatedByUserInput = false,
                        isUserInputOngoing = flowOf(false),
                    )
                )
            underTest.setTransitionState(transitionState)

            // Partially transition.
            progress.value = .4f
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(.4f))

            // Transition is at full progress.
            progress.value = 1f
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(1f))

            // Transition finishes.
            transitionState = MutableStateFlow(ObservableCommunalTransitionState.Idle(targetScene))
            underTest.setTransitionState(transitionState)
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
        }

    @Test
    fun transitionProgress_transitioningAwayFromTrackedScene() =
        testScope.runTest {
            val currentScene = CommunalSceneKey.Blank
            val targetScene = CommunalSceneKey.Communal
            val transitionProgressFlow = underTest.transitionProgressToScene(currentScene)
            val transitionProgress by collectLastValue(transitionProgressFlow)

            var transitionState =
                MutableStateFlow<ObservableCommunalTransitionState>(
                    ObservableCommunalTransitionState.Idle(currentScene)
                )
            underTest.setTransitionState(transitionState)

            // Progress starts at 0.
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))

            val progress = MutableStateFlow(0f)
            transitionState =
                MutableStateFlow(
                    ObservableCommunalTransitionState.Transition(
                        fromScene = currentScene,
                        toScene = targetScene,
                        progress = progress,
                        isInitiatedByUserInput = false,
                        isUserInputOngoing = flowOf(false),
                    )
                )
            underTest.setTransitionState(transitionState)

            // Partially transition.
            progress.value = .4f

            // This is a transition we don't care about the progress of.
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)

            // Transition is at full progress.
            progress.value = 1f
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)

            // Transition finishes.
            transitionState = MutableStateFlow(ObservableCommunalTransitionState.Idle(targetScene))
            underTest.setTransitionState(transitionState)
            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
        }

    @Test
    fun isCommunalShowing() =
        testScope.runTest {
+39 −0
Original line number Diff line number Diff line
@@ -37,6 +37,8 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

@@ -77,6 +79,29 @@ constructor(
        communalRepository.setTransitionState(transitionState)
    }

    /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */
    fun transitionProgressToScene(targetScene: CommunalSceneKey) =
        transitionState
            .flatMapLatest { state ->
                when (state) {
                    is ObservableCommunalTransitionState.Idle ->
                        flowOf(CommunalTransitionProgress.Idle(state.scene))
                    is ObservableCommunalTransitionState.Transition ->
                        if (state.toScene == targetScene) {
                            state.progress.map {
                                CommunalTransitionProgress.Transition(
                                    // Clamp the progress values between 0 and 1 as actual progress
                                    // values can be higher than 0 or lower than 1 due to a fling.
                                    progress = it.coerceIn(0.0f, 1.0f)
                                )
                            }
                        } else {
                            flowOf(CommunalTransitionProgress.OtherTransition)
                        }
                }
            }
            .distinctUntilChanged()

    /**
     * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the
     * [CommunalSceneKey.Communal].
@@ -232,3 +257,17 @@ constructor(
        }
    }
}

/** Simplified transition progress data class for tracking a single transition between scenes. */
sealed class CommunalTransitionProgress {
    /** No transition/animation is currently running. */
    data class Idle(val scene: CommunalSceneKey) : CommunalTransitionProgress()

    /** There is a transition animating to the expected scene. */
    data class Transition(
        val progress: Float,
    ) : CommunalTransitionProgress()

    /** There is a transition animating to a scene other than the expected scene. */
    data object OtherTransition : CommunalTransitionProgress()
}
+15 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.systemui.Flags
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -32,6 +33,7 @@ import kotlinx.coroutines.CoroutineDispatcher
class FromGlanceableHubTransitionInteractor
@Inject
constructor(
    private val glanceableHubTransitions: GlanceableHubTransitions,
    override val transitionRepository: KeyguardTransitionRepository,
    transitionInteractor: KeyguardTransitionInteractor,
    @Main mainDispatcher: CoroutineDispatcher,
@@ -47,6 +49,7 @@ constructor(
        if (!Flags.communalHub()) {
            return
        }
        listenForHubToLockscreen()
    }

    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -56,6 +59,18 @@ constructor(
        }
    }

    /**
     * Listens for the glanceable hub transition to lock screen and directly drives the keyguard
     * transition.
     */
    private fun listenForHubToLockscreen() {
        glanceableHubTransitions.listenForLockscreenAndHubTransition(
            transitionName = "listenForHubToLockscreen",
            transitionOwnerName = TAG,
            toScene = CommunalSceneKey.Blank
        )
    }

    companion object {
        const val TAG = "FromGlanceableHubTransitionInteractor"
        val DEFAULT_DURATION = 500.milliseconds
+19 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor

import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -62,6 +63,7 @@ constructor(
    private val flags: FeatureFlags,
    private val shadeRepository: ShadeRepository,
    private val powerInteractor: PowerInteractor,
    private val glanceableHubTransitions: GlanceableHubTransitions,
    inWindowLauncherUnlockAnimationInteractor: Lazy<InWindowLauncherUnlockAnimationInteractor>,
) :
    TransitionInteractor(
@@ -81,6 +83,7 @@ constructor(
        listenForLockscreenToPrimaryBouncerDragging()
        listenForLockscreenToAlternateBouncer()
        listenForLockscreenTransitionToCamera()
        listenForLockscreenToGlanceableHub()
    }

    /**
@@ -381,6 +384,22 @@ constructor(
        }
    }

    /**
     * Listens for transition from glanceable hub back to lock screen and directly drives the
     * keyguard transition.
     */
    private fun listenForLockscreenToGlanceableHub() {
        if (!com.android.systemui.Flags.communalHub()) {
            return
        }

        glanceableHubTransitions.listenForLockscreenAndHubTransition(
            transitionName = "listenForLockscreenToGlanceableHub",
            transitionOwnerName = TAG,
            toScene = CommunalSceneKey.Communal
        )
    }

    override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
        return ValueAnimator().apply {
            interpolator = Interpolators.LINEAR
+151 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.keyguard.domain.interactor

import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalTransitionProgress
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

class GlanceableHubTransitions
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val transitionInteractor: KeyguardTransitionInteractor,
    private val transitionRepository: KeyguardTransitionRepository,
    private val communalInteractor: CommunalInteractor,
) {
    /**
     * Listens for the glanceable hub transition to the specified scene and directly drives the
     * keyguard transition between the lockscreen and the hub.
     *
     * The glanceable hub transition progress is used as the source of truth as it cannot be driven
     * externally. The progress is used for both transitions caused by user touch input or by
     * programmatic changes.
     */
    fun listenForLockscreenAndHubTransition(
        transitionName: String,
        transitionOwnerName: String,
        toScene: CommunalSceneKey
    ) {
        val fromState: KeyguardState
        val toState: KeyguardState
        if (toScene == CommunalSceneKey.Blank) {
            fromState = KeyguardState.GLANCEABLE_HUB
            toState = KeyguardState.LOCKSCREEN
        } else {
            fromState = KeyguardState.LOCKSCREEN
            toState = KeyguardState.GLANCEABLE_HUB
        }
        var transitionId: UUID? = null
        scope.launch("$transitionOwnerName#$transitionName") {
            communalInteractor
                .transitionProgressToScene(toScene)
                .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
                .collect { pair ->
                    val (transitionProgress, lastStartedStep) = pair

                    val id = transitionId
                    if (id == null) {
                        // No transition started.
                        if (
                            transitionProgress is CommunalTransitionProgress.Transition &&
                                lastStartedStep.to == fromState
                        ) {
                            transitionId =
                                transitionRepository.startTransition(
                                    TransitionInfo(
                                        ownerName = transitionOwnerName,
                                        from = fromState,
                                        to = toState,
                                        animator = null, // transition will be manually controlled
                                    )
                                )
                        }
                    } else {
                        if (lastStartedStep.to != toState) {
                            return@collect
                        }
                        // An existing `id` means a transition is started, and calls to
                        // `updateTransition` will control it until FINISHED or CANCELED
                        val nextState: TransitionState
                        val progressFraction: Float
                        when (transitionProgress) {
                            is CommunalTransitionProgress.Idle -> {
                                if (transitionProgress.scene == toScene) {
                                    nextState = TransitionState.FINISHED
                                    progressFraction = 1f
                                } else {
                                    nextState = TransitionState.CANCELED
                                    progressFraction = 0f
                                }
                            }
                            is CommunalTransitionProgress.Transition -> {
                                nextState = TransitionState.RUNNING
                                progressFraction = transitionProgress.progress
                            }
                            is CommunalTransitionProgress.OtherTransition -> {
                                // Shouldn't happen but if another transition starts during the
                                // current one, mark the current one as canceled.
                                nextState = TransitionState.CANCELED
                                progressFraction = 0f
                            }
                        }
                        transitionRepository.updateTransition(
                            id,
                            progressFraction,
                            nextState,
                        )

                        if (
                            nextState == TransitionState.CANCELED ||
                                nextState == TransitionState.FINISHED
                        ) {
                            transitionId = null
                        }

                        // If canceled, just put the state back.
                        if (nextState == TransitionState.CANCELED) {
                            transitionRepository.startTransition(
                                TransitionInfo(
                                    ownerName = transitionOwnerName,
                                    from = toState,
                                    to = fromState,
                                    animator =
                                        ValueAnimator().apply {
                                            interpolator = Interpolators.LINEAR
                                            duration = 0
                                        }
                                )
                            )
                        }
                    }
                }
        }
    }
}
Loading