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

Commit be5367c9 authored by Matt Pietal's avatar Matt Pietal Committed by Android (Google) Code Review
Browse files

Merge "Transitions - Listen for bouncer show & hide" into tm-qpr-dev

parents 052cba91 7dc1742c
Loading
Loading
Loading
Loading
+30 −27
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import com.android.systemui.dagger.SysUISingleton
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.StatusBarState.SHADE_LOCKED
import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.WakefulnessState
@@ -50,27 +50,22 @@ constructor(

    override fun start() {
        listenForDraggingUpToBouncer()
        listenForBouncerHiding()
        listenForBouncer()
    }

    private fun listenForBouncerHiding() {
    private fun listenForBouncer() {
        scope.launch {
            keyguardInteractor.isBouncerShowing
                .sample(
                    combine(
                        keyguardInteractor.wakefulnessModel,
                        keyguardTransitionInteractor.startedKeyguardTransitionStep,
                    ) { wakefulnessModel, transitionStep ->
                        Pair(wakefulnessModel, transitionStep)
                    }
                ) { bouncerShowing, wakefulnessAndTransition ->
                    Triple(
                        bouncerShowing,
                        wakefulnessAndTransition.first,
                        wakefulnessAndTransition.second
                        ::Pair
                    ),
                    ::toTriple
                )
                }
                .collect { (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) ->
                .collect { triple ->
                    val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
                    if (
                        !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
                    ) {
@@ -91,7 +86,19 @@ constructor(
                                animator = getAnimator(),
                            )
                        )
                    } else if (
                        isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
                    ) {
                        keyguardTransitionRepository.startTransition(
                            TransitionInfo(
                                ownerName = name,
                                from = KeyguardState.LOCKSCREEN,
                                to = KeyguardState.BOUNCER,
                                animator = getAnimator(),
                            )
                        )
                    }
                    Unit
                }
        }
    }
@@ -104,24 +111,20 @@ constructor(
                    combine(
                        keyguardTransitionInteractor.finishedKeyguardState,
                        keyguardInteractor.statusBarState,
                    ) { finishedKeyguardState, statusBarState ->
                        Pair(finishedKeyguardState, statusBarState)
                    }
                ) { shadeModel, keyguardStateAndStatusBarState ->
                    Triple(
                        shadeModel,
                        keyguardStateAndStatusBarState.first,
                        keyguardStateAndStatusBarState.second
                        ::Pair
                    ),
                    ::toTriple
                )
                }
                .collect { (shadeModel, keyguardState, statusBarState) ->
                .collect { triple ->
                    val (shadeModel, keyguardState, statusBarState) = triple

                    val id = transitionId
                    if (id != null) {
                        // An existing `id` means a transition is started, and calls to
                        // `updateTransition` will control it until FINISHED
                        keyguardTransitionRepository.updateTransition(
                            id,
                            shadeModel.expansionAmount,
                            1f - shadeModel.expansionAmount,
                            if (
                                shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
                            ) {
@@ -137,7 +140,7 @@ constructor(
                        if (
                            keyguardState == KeyguardState.LOCKSCREEN &&
                                shadeModel.isUserDragging &&
                                statusBarState != SHADE_LOCKED
                                statusBarState == KEYGUARD
                        ) {
                            transitionId =
                                keyguardTransitionRepository.startTransition(
+5 −2
Original line number Diff line number Diff line
@@ -16,14 +16,17 @@

package com.android.systemui.shade;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.shade.data.repository.ShadeRepository;
import com.android.systemui.shade.data.repository.ShadeRepositoryImpl;

import dagger.Binds;
import dagger.Module;

/** Provides a {@link ShadeStateEvents} in {@link SysUISingleton} scope. */
/** Provides Shade-related events and information. */
@Module
public abstract class ShadeEventsModule {
    @Binds
    abstract ShadeStateEvents bindShadeEvents(ShadeExpansionStateManager impl);

    @Binds abstract ShadeRepository shadeRepository(ShadeRepositoryImpl impl);
}
+9 −3
Original line number Diff line number Diff line
@@ -27,11 +27,17 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged

interface ShadeRepository {
    /** ShadeModel information regarding shade expansion events */
    val shadeModel: Flow<ShadeModel>
}

/** Business logic for shade interactions */
@SysUISingleton
class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) {

    val shadeModel: Flow<ShadeModel> =
class ShadeRepositoryImpl
@Inject
constructor(shadeExpansionStateManager: ShadeExpansionStateManager) : ShadeRepository {
    override val shadeModel: Flow<ShadeModel> =
        conflatedCallbackFlow {
                val callback =
                    object : ShadeExpansionListener {
+32 −112
Original line number Diff line number Diff line
@@ -16,13 +16,10 @@

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

import android.animation.AnimationHandler.AnimationFrameCallbackProvider
import android.animation.ValueAnimator
import android.util.Log
import android.util.Log.TerribleFailure
import android.util.Log.TerribleFailureHandler
import android.view.Choreographer.FrameCallback
import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Interpolators
@@ -32,22 +29,17 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.util.KeyguardTransitionRunner
import com.google.common.truth.Truth.assertThat
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -60,12 +52,14 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
    private lateinit var underTest: KeyguardTransitionRepository
    private lateinit var oldWtfHandler: TerribleFailureHandler
    private lateinit var wtfHandler: WtfHandler
    private lateinit var runner: KeyguardTransitionRunner

    @Before
    fun setUp() {
        underTest = KeyguardTransitionRepositoryImpl()
        wtfHandler = WtfHandler()
        oldWtfHandler = Log.setWtfHandler(wtfHandler)
        runner = KeyguardTransitionRunner(underTest)
    }

    @After
@@ -75,56 +69,37 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {

    @Test
    fun `startTransition runs animator to completion`() =
        runBlocking(IMMEDIATE) {
            val (animator, provider) = setupAnimator(this)

        TestScope().runTest {
            val steps = mutableListOf<TransitionStep>()
            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)

            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))

            val startTime = System.currentTimeMillis()
            while (animator.isRunning()) {
                yield()
                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
                }
            }
            runner.startTransition(
                this,
                TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
                maxFrames = 100
            )

            assertSteps(steps, listWithStep(BigDecimal(.1)), AOD, LOCKSCREEN)

            job.cancel()
            provider.stop()
        }

    @Test
    @FlakyTest(bugId = 260213291)
    fun `starting second transition will cancel the first transition`() {
        runBlocking(IMMEDIATE) {
            val (animator, provider) = setupAnimator(this)

    fun `starting second transition will cancel the first transition`() =
        TestScope().runTest {
            val steps = mutableListOf<TransitionStep>()
            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)

            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
            // 3 yields(), alternating with the animator, results in a value 0.1, which can be
            // canceled and tested against
            yield()
            yield()
            yield()
            runner.startTransition(
                this,
                TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, getAnimator()),
                maxFrames = 3,
            )

            // Now start 2nd transition, which will interrupt the first
            val job2 = underTest.transition(LOCKSCREEN, AOD).onEach { steps.add(it) }.launchIn(this)
            val (animator2, provider2) = setupAnimator(this)
            underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, AOD, animator2))

            val startTime = System.currentTimeMillis()
            while (animator2.isRunning()) {
                yield()
                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
                }
            }
            runner.startTransition(
                this,
                TransitionInfo(OWNER_NAME, LOCKSCREEN, AOD, getAnimator()),
            )

            val firstTransitionSteps = listWithStep(step = BigDecimal(.1), stop = BigDecimal(.1))
            assertSteps(steps.subList(0, 4), firstTransitionSteps, AOD, LOCKSCREEN)
@@ -134,31 +109,25 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {

            job.cancel()
            job2.cancel()
            provider.stop()
            provider2.stop()
        }
        }

    @Test
    fun `Null animator enables manual control with updateTransition`() =
        runBlocking(IMMEDIATE) {
        TestScope().runTest {
            val steps = mutableListOf<TransitionStep>()
            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)

            val uuid =
                underTest.startTransition(
                    TransitionInfo(
                        ownerName = OWNER_NAME,
                        from = AOD,
                        to = LOCKSCREEN,
                        animator = null,
                    )
                    TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator = null)
                )
            runCurrent()

            checkNotNull(uuid).let {
                underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
                underTest.updateTransition(it, 1f, TransitionState.FINISHED)
            }
            runCurrent()

            assertThat(steps.size).isEqualTo(3)
            assertThat(steps[0])
@@ -256,57 +225,11 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
        assertThat(wtfHandler.failed).isFalse()
    }

    private fun setupAnimator(
        scope: CoroutineScope
    ): Pair<ValueAnimator, TestFrameCallbackProvider> {
        val animator =
            ValueAnimator().apply {
    private fun getAnimator(): ValueAnimator {
        return ValueAnimator().apply {
            setInterpolator(Interpolators.LINEAR)
                setDuration(ANIMATION_DURATION)
            }

        val provider = TestFrameCallbackProvider(animator, scope)
        provider.start()

        return Pair(animator, provider)
    }

    /** Gives direct control over ValueAnimator. See [AnimationHandler] */
    private class TestFrameCallbackProvider(
        private val animator: ValueAnimator,
        private val scope: CoroutineScope,
    ) : AnimationFrameCallbackProvider {

        private var frameCount = 1L
        private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
        private var job: Job? = null

        fun start() {
            animator.getAnimationHandler().setProvider(this)

            job =
                scope.launch {
                    frames.collect {
                        // Delay is required for AnimationHandler to properly register a callback
                        yield()
                        val (frameNumber, callback) = it
                        callback?.doFrame(frameNumber)
                    }
                }
        }

        fun stop() {
            job?.cancel()
            animator.getAnimationHandler().setProvider(null)
        }

        override fun postFrameCallback(cb: FrameCallback) {
            frames.value = Pair(frameCount++, cb)
            setDuration(10)
        }
        override fun postCommitCallback(runnable: Runnable) {}
        override fun getFrameTime() = frameCount
        override fun getFrameDelay() = 1L
        override fun setFrameDelay(delay: Long) {}
    }

    private class WtfHandler : TerribleFailureHandler {
@@ -317,9 +240,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
    }

    companion object {
        private const val MAX_TEST_DURATION = 100L
        private const val ANIMATION_DURATION = 10L
        private const val OWNER_NAME = "Test"
        private val IMMEDIATE = Dispatchers.Main.immediate
        private const val OWNER_NAME = "KeyguardTransitionRunner"
    }
}
+140 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Interpolators
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.keyguard.util.KeyguardTransitionRunner
import com.android.systemui.shade.data.repository.FakeShadeRepository
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

/**
 * Class for testing user journeys through the interactors. They will all be activated during setup,
 * to ensure the expected transitions are still triggered.
 */
@SmallTest
@RunWith(JUnit4::class)
class KeyguardTransitionScenariosTest : SysuiTestCase() {
    private lateinit var testScope: TestScope

    private lateinit var keyguardRepository: FakeKeyguardRepository
    private lateinit var shadeRepository: ShadeRepository

    // Used to issue real transition steps for test input
    private lateinit var runner: KeyguardTransitionRunner
    private lateinit var transitionRepository: KeyguardTransitionRepository

    // Used to verify transition requests for test output
    @Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository

    private lateinit var lockscreenBouncerTransitionInteractor:
        LockscreenBouncerTransitionInteractor

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        testScope = TestScope()

        keyguardRepository = FakeKeyguardRepository()
        shadeRepository = FakeShadeRepository()

        /* Used to issue full transition steps, to better simulate a real device */
        transitionRepository = KeyguardTransitionRepositoryImpl()
        runner = KeyguardTransitionRunner(transitionRepository)

        lockscreenBouncerTransitionInteractor =
            LockscreenBouncerTransitionInteractor(
                scope = testScope,
                keyguardInteractor = KeyguardInteractor(keyguardRepository),
                shadeRepository = shadeRepository,
                keyguardTransitionRepository = mockTransitionRepository,
                keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
            )
        lockscreenBouncerTransitionInteractor.start()
    }

    @Test
    fun `LOCKSCREEN to BOUNCER via bouncer showing call`() =
        testScope.runTest {
            // GIVEN a device that has at least woken up
            keyguardRepository.setWakefulnessModel(startingToWake())
            runCurrent()

            // GIVEN a transition has run to LOCKSCREEN
            runner.startTransition(
                testScope,
                TransitionInfo(
                    ownerName = "",
                    from = KeyguardState.OFF,
                    to = KeyguardState.LOCKSCREEN,
                    animator =
                        ValueAnimator().apply {
                            duration = 10
                            interpolator = Interpolators.LINEAR
                        },
                )
            )
            runCurrent()

            // WHEN the bouncer is set to show
            keyguardRepository.setBouncerShowing(true)
            runCurrent()

            val info =
                withArgCaptor<TransitionInfo> {
                    verify(mockTransitionRepository).startTransition(capture())
                }
            // THEN a transition to BOUNCER should occur
            assertThat(info.ownerName).isEqualTo("LockscreenBouncerTransitionInteractor")
            assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
            assertThat(info.to).isEqualTo(KeyguardState.BOUNCER)
            assertThat(info.animator).isNotNull()

            coroutineContext.cancelChildren()
        }

    private fun startingToWake() =
        WakefulnessModel(
            WakefulnessState.STARTING_TO_WAKE,
            true,
            WakeSleepReason.OTHER,
            WakeSleepReason.OTHER
        )
}
Loading