Loading packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +30 −27 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 ) { Loading @@ -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 } } } Loading @@ -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 ) { Loading @@ -137,7 +140,7 @@ constructor( if ( keyguardState == KeyguardState.LOCKSCREEN && shadeModel.isUserDragging && statusBarState != SHADE_LOCKED statusBarState == KEYGUARD ) { transitionId = keyguardTransitionRepository.startTransition( Loading packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java +5 −2 Original line number Diff line number Diff line Loading @@ -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); } packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +9 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +32 −112 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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]) Loading Loading @@ -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 { Loading @@ -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" } } packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt 0 → 100644 +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
packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +30 −27 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 ) { Loading @@ -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 } } } Loading @@ -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 ) { Loading @@ -137,7 +140,7 @@ constructor( if ( keyguardState == KeyguardState.LOCKSCREEN && shadeModel.isUserDragging && statusBarState != SHADE_LOCKED statusBarState == KEYGUARD ) { transitionId = keyguardTransitionRepository.startTransition( Loading
packages/SystemUI/src/com/android/systemui/shade/ShadeEventsModule.java +5 −2 Original line number Diff line number Diff line Loading @@ -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); }
packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +9 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading
packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +32 −112 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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]) Loading Loading @@ -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 { Loading @@ -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" } }
packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt 0 → 100644 +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 ) }