Merge "Transitions - Listen for bouncer show & hide" into tm-qpr-dev
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
.collect { (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) ->
|
||||
::Pair
|
||||
),
|
||||
::toTriple
|
||||
)
|
||||
.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
|
||||
)
|
||||
}
|
||||
.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 @@ constructor(
|
||||
if (
|
||||
keyguardState == KeyguardState.LOCKSCREEN &&
|
||||
shadeModel.isUserDragging &&
|
||||
statusBarState != SHADE_LOCKED
|
||||
statusBarState == KEYGUARD
|
||||
) {
|
||||
transitionId =
|
||||
keyguardTransitionRepository.startTransition(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
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 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,14 @@ class FakeKeyguardRepository : KeyguardRepository {
|
||||
_dozeAmount.value = dozeAmount
|
||||
}
|
||||
|
||||
fun setWakefulnessModel(model: WakefulnessModel) {
|
||||
_wakefulnessModel.value = model
|
||||
}
|
||||
|
||||
fun setBouncerShowing(isShowing: Boolean) {
|
||||
_isBouncerShowing.value = isShowing
|
||||
}
|
||||
|
||||
fun setBiometricUnlockState(state: BiometricUnlockModel) {
|
||||
_biometricUnlockState.tryEmit(state)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user