Merge "Transitions - Listen for bouncer show & hide" into tm-qpr-dev
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
index 3218f96..5cb7d70 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -22,7 +22,7 @@
 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 @@
 
     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
-                    )
-                }
-                .collect { (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) ->
+                        ::Pair
+                    ),
+                    ::toTriple
+                )
+                .collect { triple ->
+                    val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
                     if (
                         !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
                     ) {
@@ -91,7 +86,19 @@
                                 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 @@
                     combine(
                         keyguardTransitionInteractor.finishedKeyguardState,
                         keyguardInteractor.statusBarState,
-                    ) { finishedKeyguardState, statusBarState ->
-                        Pair(finishedKeyguardState, statusBarState)
-                    }
-                ) { shadeModel, keyguardStateAndStatusBarState ->
-                    Triple(
-                        shadeModel,
-                        keyguardStateAndStatusBarState.first,
-                        keyguardStateAndStatusBarState.second
-                    )
-                }
-                .collect { (shadeModel, keyguardState, statusBarState) ->
+                        ::Pair
+                    ),
+                    ::toTriple
+                )
+                .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 @@
                         if (
                             keyguardState == KeyguardState.LOCKSCREEN &&
                                 shadeModel.isUserDragging &&
-                                statusBarState != SHADE_LOCKED
+                                statusBarState == KEYGUARD
                         ) {
                             transitionId =
                                 keyguardTransitionRepository.startTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
index 959c339..f87a1ed 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java
@@ -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);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index 09019a6..bf7a2be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -27,11 +27,17 @@
 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 {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index ce9c1da..5d2f0eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -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.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 @@
     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 @@
 
     @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 @@
 
             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 @@
         assertThat(wtfHandler.failed).isFalse()
     }
 
-    private fun setupAnimator(
-        scope: CoroutineScope
-    ): Pair<ValueAnimator, TestFrameCallbackProvider> {
-        val animator =
-            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)
-                    }
-                }
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(10)
         }
-
-        fun stop() {
-            job?.cancel()
-            animator.getAnimationHandler().setProvider(null)
-        }
-
-        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) {}
     }
 
     private class WtfHandler : TerribleFailureHandler {
@@ -317,9 +240,6 @@
     }
 
     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"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
new file mode 100644
index 0000000..a6cf840
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -0,0 +1,140 @@
+/*
+ * 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
+        )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
new file mode 100644
index 0000000..c88f84a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/util/KeyguardTransitionRunner.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.util
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider
+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 kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+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 {
+
+    private var frameCount = 1L
+    private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
+    private var job: Job? = null
+    private var isTerminated = false
+
+    /**
+     * 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) {
+        // AnimationHandler uses ThreadLocal storage, and ValueAnimators MUST start from main
+        // thread
+        withContext(Dispatchers.Main) {
+            info.animator!!.getAnimationHandler().setProvider(this@KeyguardTransitionRunner)
+        }
+
+        job =
+            scope.launch {
+                frames.collect {
+                    val (frameNumber, callback) = it
+
+                    isTerminated = frameNumber >= maxFrames
+                    if (!isTerminated) {
+                        withContext(Dispatchers.Main) { callback?.doFrame(frameNumber) }
+                    }
+                }
+            }
+        withContext(Dispatchers.Main) { repository.startTransition(info) }
+
+        waitUntilComplete(info.animator!!)
+    }
+
+    suspend private fun waitUntilComplete(animator: ValueAnimator) {
+        withContext(Dispatchers.Main) {
+            val startTime = System.currentTimeMillis()
+            while (!isTerminated && animator.isRunning()) {
+                delay(1)
+                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                }
+            }
+
+            animator.getAnimationHandler().setProvider(null)
+        }
+
+        job?.cancel()
+    }
+
+    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 {
+        private const val MAX_TEST_DURATION = 100L
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 5c2a915..5501949 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -120,6 +120,14 @@
         _dozeAmount.value = dozeAmount
     }
 
+    fun setWakefulnessModel(model: WakefulnessModel) {
+        _wakefulnessModel.value = model
+    }
+
+    fun setBouncerShowing(isShowing: Boolean) {
+        _isBouncerShowing.value = isShowing
+    }
+
     fun setBiometricUnlockState(state: BiometricUnlockModel) {
         _biometricUnlockState.tryEmit(state)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
new file mode 100644
index 0000000..2c0a8fd
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.shade.data.repository
+
+import com.android.systemui.shade.domain.model.ShadeModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Fake implementation of [KeyguardRepository] */
+class FakeShadeRepository : ShadeRepository {
+
+    private val _shadeModel = MutableStateFlow(ShadeModel())
+    override val shadeModel: Flow<ShadeModel> = _shadeModel
+
+    fun setShadeModel(model: ShadeModel) {
+        _shadeModel.value = model
+    }
+}