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

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

Merge "Add forceFinishCurrentTransition and hook it up to screen power state." into main

parents a33c539a a102090f
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -1784,3 +1784,13 @@ flag {
      purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "keyguard_transition_force_finish_on_screen_off"
    namespace: "systemui"
    description: "Forces KTF transitions to finish if the screen turns all the way off."
    bug: "331636736"
    metadata {
      purpose: PURPOSE_BUGFIX
    }
}
 No newline at end of file
+58 −6
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.systemui.keyguard.data.repository

import android.animation.Animator
import android.animation.ValueAnimator
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.app.animation.Interpolators
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -41,6 +44,8 @@ import com.google.common.truth.Truth.assertThat
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.dropWhile
@@ -53,6 +58,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -65,6 +71,8 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
    private lateinit var underTest: KeyguardTransitionRepository
    private lateinit var runner: KeyguardTransitionRunner

    private val animatorListener = mock<Animator.AnimatorListener>()

    @Before
    fun setUp() {
        underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main)
@@ -80,7 +88,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
            runner.startTransition(
                this,
                TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
                maxFrames = 100
                maxFrames = 100,
            )

            assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)
@@ -107,7 +115,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
                    LOCKSCREEN,
                    AOD,
                    getAnimator(),
                    TransitionModeOnCanceled.LAST_VALUE
                    TransitionModeOnCanceled.LAST_VALUE,
                ),
            )

@@ -142,7 +150,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
                    LOCKSCREEN,
                    AOD,
                    getAnimator(),
                    TransitionModeOnCanceled.RESET
                    TransitionModeOnCanceled.RESET,
                ),
            )

@@ -177,7 +185,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
                    LOCKSCREEN,
                    AOD,
                    getAnimator(),
                    TransitionModeOnCanceled.REVERSE
                    TransitionModeOnCanceled.REVERSE,
                ),
            )

@@ -476,6 +484,49 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
            assertThat(steps.size).isEqualTo(3)
        }

    @Test
    @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF)
    fun forceFinishCurrentTransition_noFurtherStepsEmitted() =
        testScope.runTest {
            val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })

            var sentForceFinish = false

            runner.startTransition(
                this,
                TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
                maxFrames = 100,
                // Force-finish on the second frame.
                frameCallback = { frameNumber ->
                    if (!sentForceFinish && frameNumber > 1) {
                        testScope.launch { underTest.forceFinishCurrentTransition() }
                        sentForceFinish = true
                    }
                },
            )

            val lastTwoRunningSteps =
                steps.filter { it.transitionState == TransitionState.RUNNING }.takeLast(2)

            // Make sure we stopped emitting RUNNING steps early, but then emitted a final 1f step.
            assertTrue(lastTwoRunningSteps[0].value < 0.5f)
            assertTrue(lastTwoRunningSteps[1].value == 1f)

            assertEquals(steps.last().from, AOD)
            assertEquals(steps.last().to, LOCKSCREEN)
            assertEquals(steps.last().transitionState, TransitionState.FINISHED)
        }

    @Test
    @EnableFlags(Flags.FLAG_KEYGUARD_TRANSITION_FORCE_FINISH_ON_SCREEN_OFF)
    fun forceFinishCurrentTransition_noTransitionStarted_noStepsEmitted() =
        testScope.runTest {
            val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })

            underTest.forceFinishCurrentTransition()
            assertEquals(0, steps.size)
        }

    private fun listWithStep(
        step: BigDecimal,
        start: BigDecimal = BigDecimal.ZERO,
@@ -505,7 +556,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
                    to,
                    fractions[0].toFloat(),
                    TransitionState.STARTED,
                    OWNER_NAME
                    OWNER_NAME,
                )
            )
        fractions.forEachIndexed { index, fraction ->
@@ -519,7 +570,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
                        to,
                        fraction.toFloat(),
                        TransitionState.RUNNING,
                        OWNER_NAME
                        OWNER_NAME,
                    )
                )
        }
@@ -538,6 +589,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
        return ValueAnimator().apply {
            setInterpolator(Interpolators.LINEAR)
            setDuration(10)
            addListener(animatorListener)
        }
    }

+19 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.animation.ValueAnimator
import android.view.Choreographer.FrameCallback
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.TransitionInfo
import java.util.function.Consumer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -35,9 +36,8 @@ import org.junit.Assert.fail
 * Gives direct control over ValueAnimator, in order to make transition tests deterministic. See
 * [AnimationHandler]. Animators are required to be run on the main thread, so dispatch accordingly.
 */
class KeyguardTransitionRunner(
    val repository: KeyguardTransitionRepository,
) : AnimationFrameCallbackProvider {
class KeyguardTransitionRunner(val repository: KeyguardTransitionRepository) :
    AnimationFrameCallbackProvider {

    private var frameCount = 1L
    private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
@@ -48,7 +48,12 @@ class KeyguardTransitionRunner(
     * For transitions being directed by an animator. Will control the number of frames being
     * generated so the values are deterministic.
     */
    suspend fun startTransition(scope: CoroutineScope, info: TransitionInfo, maxFrames: Int = 100) {
    suspend fun startTransition(
        scope: CoroutineScope,
        info: TransitionInfo,
        maxFrames: Int = 100,
        frameCallback: Consumer<Long>? = null,
    ) {
        // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main
        // thread
        withContext(Dispatchers.Main) {
@@ -62,7 +67,12 @@ class KeyguardTransitionRunner(

                    isTerminated = frameNumber >= maxFrames
                    if (!isTerminated) {
                        try {
                            withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
                            frameCallback?.accept(frameNumber)
                        } catch (e: IllegalStateException) {
                            e.printStackTrace()
                        }
                    }
                }
            }
@@ -90,9 +100,13 @@ class KeyguardTransitionRunner(
    override fun postFrameCallback(cb: FrameCallback) {
        frames.value = Pair(frameCount++, cb)
    }

    override fun postCommitCallback(runnable: Runnable) {}

    override fun getFrameTime() = frameCount

    override fun getFrameDelay() = 1L

    override fun setFrameDelay(delay: Long) {}

    companion object {
+43 −2
Original line number Diff line number Diff line
@@ -114,6 +114,18 @@ interface KeyguardTransitionRepository {
        @FloatRange(from = 0.0, to = 1.0) value: Float,
        state: TransitionState,
    )

    /**
     * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that
     * otherwise would have been emitted.
     *
     * When the screen is off, upcoming performance changes cause all Animators to cease emitting
     * frames, which means the Animator passed to [startTransition] will never finish if it was
     * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when
     * the screen isn't even on. As long as we emit FINISHED, everything should end up in the
     * correct state.
     */
    suspend fun forceFinishCurrentTransition()
}

@SysUISingleton
@@ -134,6 +146,7 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR
    override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
    private var lastStep: TransitionStep = TransitionStep()
    private var lastAnimator: ValueAnimator? = null
    private var animatorListener: AnimatorListenerAdapter? = null

    private val withContextMutex = Mutex()
    private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
@@ -233,7 +246,7 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR
                    )
                }

                val adapter =
                animatorListener =
                    object : AnimatorListenerAdapter() {
                        override fun onAnimationStart(animation: Animator) {
                            emitTransition(
@@ -254,9 +267,10 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR
                            animator.removeListener(this)
                            animator.removeUpdateListener(updateListener)
                            lastAnimator = null
                            animatorListener = null
                        }
                    }
                animator.addListener(adapter)
                animator.addListener(animatorListener)
                animator.addUpdateListener(updateListener)
                animator.start()
                return@withContext null
@@ -290,6 +304,33 @@ constructor(@Main val mainDispatcher: CoroutineDispatcher) : KeyguardTransitionR
        }
    }

    override suspend fun forceFinishCurrentTransition() {
        withContextMutex.lock()

        if (lastAnimator?.isRunning != true) {
            return
        }

        return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) {
            withContextMutex.unlock()

            Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.")

            lastAnimator?.apply {
                // Cancel the animator, but remove listeners first so we don't emit CANCELED.
                removeAllListeners()
                cancel()

                // Emit a final 1f RUNNING step to ensure that any transitions not listening for a
                // FINISHED step end up in the right end state.
                emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING))

                // Ask the listener to emit FINISHED and clean up its state.
                animatorListener?.onAnimationEnd(this)
            }
        }
    }

    private suspend fun updateTransitionInternal(
        transitionId: UUID,
        @FloatRange(from = 0.0, to = 1.0) value: Float,
+17 −1
Original line number Diff line number Diff line
@@ -19,8 +19,10 @@ package com.android.systemui.keyguard.domain.interactor

import android.annotation.SuppressLint
import android.util.Log
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
@@ -30,6 +32,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.ScreenPowerState
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
@@ -59,7 +63,6 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import com.android.app.tracing.coroutines.launchTraced as launch

/** Encapsulates business-logic related to the keyguard transitions. */
@OptIn(ExperimentalCoroutinesApi::class)
@@ -70,6 +73,7 @@ constructor(
    @Application val scope: CoroutineScope,
    private val repository: KeyguardTransitionRepository,
    private val sceneInteractor: SceneInteractor,
    private val powerInteractor: PowerInteractor,
) {
    private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>()

@@ -188,6 +192,18 @@ constructor(
                    }
                }
        }

        if (keyguardTransitionForceFinishOnScreenOff()) {
            /**
             * If the screen is turning off, finish the current transition immediately. Further
             * frames won't be visible anyway.
             */
            scope.launch {
                powerInteractor.screenPowerState
                    .filter { it == ScreenPowerState.SCREEN_TURNING_OFF }
                    .collect { repository.forceFinishCurrentTransition() }
            }
        }
    }

    fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> {
Loading