Fix new lightreveal transition

- Lightreveal will now play independently instead of being directly tied
to Transition steps. This is needed because the transition AOD => GONE
has actually two parts of the transition AOD => LOCKSCREEN then
LOCKSCREEN => GONE would otherwise cancel the animation when the second
part of the transition starts.
- LightReveal will now play backwards from it's current state when
unlocking/locking quickly one after another (instead cancel/restart)
- LightReveal will now play correctly (from tap) when AOD has not yet
entered low power mode
- Power off will now animate correctly even if the device was unlocked
with tap

Test: added unit tests
Bug: b/292087855

Change-Id: Ie25efca4a829dede2da80a518e425ec9f314d6d9
This commit is contained in:
Andreas Miko
2023-07-06 20:03:48 +02:00
parent 8d9f729ae6
commit 3871e2d327
10 changed files with 257 additions and 178 deletions

View File

@@ -20,10 +20,13 @@ package com.android.systemui.keyguard.data.repository
import android.content.Context
import android.graphics.Point
import androidx.core.animation.Animator
import androidx.core.animation.ValueAnimator
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LiftReveal
import com.android.systemui.statusbar.LightRevealEffect
@@ -31,9 +34,12 @@ import com.android.systemui.statusbar.PowerButtonReveal
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -52,6 +58,10 @@ interface LightRevealScrimRepository {
* at the current screen position of the appropriate sensor.
*/
val revealEffect: Flow<LightRevealEffect>
val revealAmount: Flow<Float>
fun startRevealAmountAnimator(reveal: Boolean)
}
@SysUISingleton
@@ -108,13 +118,30 @@ constructor(
/** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */
private val nonBiometricRevealEffect: Flow<LightRevealEffect?> =
keyguardRepository.wakefulness.flatMapLatest { wakefulnessModel ->
when {
wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
wakefulnessModel.isAwakeFromTap() -> tapRevealEffect
else -> flowOf(LiftReveal)
keyguardRepository.wakefulness
.filter { it.isStartingToWake() || it.isStartingToSleep() }
.flatMapLatest { wakefulnessModel ->
when {
wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
wakefulnessModel.isWakingFrom(TAP) -> tapRevealEffect
else -> flowOf(LiftReveal)
}
}
}
private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 500 }
override val revealAmount: Flow<Float> = callbackFlow {
val updateListener =
Animator.AnimatorUpdateListener {
trySend((it as ValueAnimator).animatedValue as Float)
}
revealAmountAnimator.addUpdateListener(updateListener)
awaitClose { revealAmountAnimator.removeUpdateListener(updateListener) }
}
override fun startRevealAmountAnimator(reveal: Boolean) {
if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
}
override val revealEffect =
combine(

View File

@@ -17,28 +17,44 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
@SysUISingleton
class LightRevealScrimInteractor
@Inject
constructor(
transitionRepository: KeyguardTransitionRepository,
transitionInteractor: KeyguardTransitionInteractor,
lightRevealScrimRepository: LightRevealScrimRepository,
private val transitionInteractor: KeyguardTransitionInteractor,
private val lightRevealScrimRepository: LightRevealScrimRepository,
@Application private val scope: CoroutineScope,
) {
init {
listenForStartedKeyguardTransitionStep()
}
private fun listenForStartedKeyguardTransitionStep() {
scope.launch {
transitionInteractor.startedKeyguardTransitionStep.collect {
if (willTransitionChangeEndState(it)) {
lightRevealScrimRepository.startRevealAmountAnimator(
willBeRevealedInState(it.to)
)
}
}
}
}
/**
* Whenever a keyguard transition starts, sample the latest reveal effect from the repository
* and use that for the starting transition.
@@ -54,17 +70,7 @@ constructor(
lightRevealScrimRepository.revealEffect
)
/**
* The reveal amount to use for the light reveal scrim, which is derived from the keyguard
* transition steps.
*/
val revealAmount: Flow<Float> =
transitionRepository.transitions
// Only listen to transitions that change the reveal amount.
.filter { willTransitionAffectRevealAmount(it) }
// Use the transition amount as the reveal amount, inverting it if we're transitioning
// to a non-revealed (hidden) state.
.map { step -> if (willBeRevealedInState(step.to)) step.value else 1f - step.value }
val revealAmount = lightRevealScrimRepository.revealAmount
companion object {
@@ -72,7 +78,7 @@ constructor(
* Whether the transition requires a change in the reveal amount of the light reveal scrim.
* If not, we don't care about the transition and don't need to listen to it.
*/
fun willTransitionAffectRevealAmount(transition: TransitionStep): Boolean {
fun willTransitionChangeEndState(transition: TransitionStep): Boolean {
return willBeRevealedInState(transition.from) != willBeRevealedInState(transition.to)
}

View File

@@ -16,6 +16,13 @@
package com.android.systemui.keyguard.shared.model
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.WakeSleepReason.GESTURE
import com.android.systemui.keyguard.shared.model.WakeSleepReason.POWER_BUTTON
import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
import com.android.systemui.keyguard.shared.model.WakefulnessState.ASLEEP
import com.android.systemui.keyguard.shared.model.WakefulnessState.AWAKE
import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_SLEEP
import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_WAKE
/** Model device wakefulness states. */
data class WakefulnessModel(
@@ -23,33 +30,31 @@ data class WakefulnessModel(
val lastWakeReason: WakeSleepReason,
val lastSleepReason: WakeSleepReason,
) {
fun isStartingToWake() = state == WakefulnessState.STARTING_TO_WAKE
fun isStartingToWake() = state == STARTING_TO_WAKE
fun isStartingToSleep() = state == WakefulnessState.STARTING_TO_SLEEP
fun isStartingToSleep() = state == STARTING_TO_SLEEP
private fun isAsleep() = state == WakefulnessState.ASLEEP
private fun isAsleep() = state == ASLEEP
private fun isAwake() = state == AWAKE
fun isStartingToWakeOrAwake() = isStartingToWake() || isAwake()
fun isStartingToSleepOrAsleep() = isStartingToSleep() || isAsleep()
fun isDeviceInteractive() = !isAsleep()
fun isStartingToWakeOrAwake() = isStartingToWake() || state == WakefulnessState.AWAKE
fun isWakingFrom(wakeSleepReason: WakeSleepReason) =
isStartingToWake() && lastWakeReason == wakeSleepReason
fun isStartingToSleepFromPowerButton() =
isStartingToSleep() && lastWakeReason == WakeSleepReason.POWER_BUTTON
fun isWakingFromPowerButton() =
isStartingToWake() && lastWakeReason == WakeSleepReason.POWER_BUTTON
fun isStartingToSleepFrom(wakeSleepReason: WakeSleepReason) =
isStartingToSleep() && lastSleepReason == wakeSleepReason
fun isTransitioningFromPowerButton() =
isStartingToSleepFromPowerButton() || isWakingFromPowerButton()
fun isAwakeFromTap() =
state == WakefulnessState.STARTING_TO_WAKE && lastWakeReason == WakeSleepReason.TAP
isStartingToSleepFrom(POWER_BUTTON) || isWakingFrom(POWER_BUTTON)
fun isDeviceInteractiveFromTapOrGesture(): Boolean {
return isDeviceInteractive() &&
(lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
return isDeviceInteractive() && (lastWakeReason == TAP || lastWakeReason == GESTURE)
}
companion object {

View File

@@ -20,20 +20,18 @@ import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** View-model for the keyguard indication area view */
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardIndicationAreaViewModel
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
private val keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
bottomAreaInteractor: KeyguardBottomAreaInteractor,
keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {

View File

@@ -19,12 +19,14 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.keyguard.domain.interactor.LightRevealScrimInteractor
import com.android.systemui.statusbar.LightRevealEffect
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
/**
* Models UI state for the light reveal scrim, which is used during screen on and off animations to
* draw a gradient that reveals/hides the contents of the screen.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class LightRevealScrimViewModel @Inject constructor(interactor: LightRevealScrimInteractor) {
val lightRevealEffect: Flow<LightRevealEffect> = interactor.lightRevealEffect
val revealAmount: Flow<Float> = interactor.revealAmount

View File

@@ -16,6 +16,7 @@
package com.android.systemui.shade
import android.graphics.Point
import android.hardware.display.AmbientDisplayConfiguration
import android.os.PowerManager
import android.provider.Settings
@@ -25,6 +26,7 @@ import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.DozeInteractor
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -52,6 +54,7 @@ class PulsingGestureListener @Inject constructor(
private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
private val statusBarStateController: StatusBarStateController,
private val shadeLogger: ShadeLogger,
private val dozeInteractor: DozeInteractor,
userTracker: UserTracker,
tunerService: TunerService,
dumpManager: DumpManager
@@ -86,6 +89,7 @@ class PulsingGestureListener @Inject constructor(
shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
if (proximityIsNotNear && isNotAFalseTap) {
shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
dozeInteractor.setLastTapToWakePosition(Point(e.x.toInt(), e.y.toInt()))
powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP)
}
return true

View File

@@ -17,31 +17,43 @@
package com.android.systemui.keyguard.data.repository
import android.graphics.Point
import androidx.test.ext.junit.runners.AndroidJUnit4
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.core.animation.AnimatorTestRule
import androidx.test.filters.SmallTest
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
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.statusbar.CircleReveal
import com.android.systemui.statusbar.LightRevealEffect
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
@SmallTest
@RoboPilotTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
class LightRevealScrimRepositoryTest : SysuiTestCase() {
private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
private lateinit var underTest: LightRevealScrimRepositoryImpl
@get:Rule val animatorTestRule = AnimatorTestRule()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -50,112 +62,127 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() {
}
@Test
fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() =
runTest {
val values = mutableListOf<LightRevealEffect>()
val job = launch { underTest.revealEffect.collect { values.add(it) } }
fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest {
val values = mutableListOf<LightRevealEffect>()
val job = launch { underTest.revealEffect.collect { values.add(it) } }
// We should initially emit the default reveal effect.
runCurrent()
values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
// The source and sensor locations are still null, so we should still be using the
// default reveal despite a biometric unlock.
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
fakeKeyguardRepository.setWakefulnessModel(
WakefulnessModel(
WakefulnessState.STARTING_TO_WAKE,
WakeSleepReason.OTHER,
WakeSleepReason.OTHER
)
)
// We should initially emit the default reveal effect.
runCurrent()
values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
// We got a source but still have no sensor locations, so should be sticking with
// the default effect.
fakeKeyguardRepository.setBiometricUnlockSource(
BiometricUnlockSource.FINGERPRINT_SENSOR
)
// The source and sensor locations are still null, so we should still be using the
// default reveal despite a biometric unlock.
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
)
// We got a location for the face sensor, but we unlocked with fingerprint.
val faceLocation = Point(250, 0)
fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
// We got a source but still have no sensor locations, so should be sticking with
// the default effect.
fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
)
// Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
val fingerprintLocation = Point(500, 500)
fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
fakeKeyguardRepository.setBiometricUnlockSource(
BiometricUnlockSource.FINGERPRINT_SENSOR
)
fakeKeyguardRepository.setBiometricUnlockState(
BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
)
// We got a location for the face sensor, but we unlocked with fingerprint.
val faceLocation = Point(250, 0)
fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
// We should now have switched to the circle reveal, at the fingerprint location.
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == fingerprintLocation.x &&
it.centerY == fingerprintLocation.y
},
)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
)
// Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
val valuesPrevSize = values.size
fakeKeyguardRepository.setBiometricUnlockState(
BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
)
fakeKeyguardRepository.setBiometricUnlockState(
BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
)
assertEquals(valuesPrevSize, values.size)
// Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
val fingerprintLocation = Point(500, 500)
fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
// Non-biometric unlock, we should return to the default reveal.
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
// We should now have switched to the circle reveal, at the fingerprint location.
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == fingerprintLocation.x &&
it.centerY == fingerprintLocation.y
},
)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == fingerprintLocation.x &&
it.centerY == fingerprintLocation.y
},
{ it == DEFAULT_REVEAL_EFFECT },
)
// Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
val valuesPrevSize = values.size
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
fakeKeyguardRepository.setBiometricUnlockState(
BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
)
assertEquals(valuesPrevSize, values.size)
// We already have a face location, so switching to face source should update the
// CircleReveal.
fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
runCurrent()
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
runCurrent()
// Non-biometric unlock, we should return to the default reveal.
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == fingerprintLocation.x &&
it.centerY == fingerprintLocation.y
},
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == faceLocation.x &&
it.centerY == faceLocation.y
},
)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == fingerprintLocation.x &&
it.centerY == fingerprintLocation.y
},
{ it == DEFAULT_REVEAL_EFFECT },
)
job.cancel()
// We already have a face location, so switching to face source should update the
// CircleReveal.
fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
runCurrent()
fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
runCurrent()
values.assertEffectsMatchPredicates(
{ it == DEFAULT_REVEAL_EFFECT },
{
it is CircleReveal &&
it.centerX == fingerprintLocation.x &&
it.centerY == fingerprintLocation.y
},
{ it == DEFAULT_REVEAL_EFFECT },
{ it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y },
)
job.cancel()
}
@Test
@TestableLooper.RunWithLooper(setAsMainLooper = true)
fun revealAmount_emitsTo1AfterAnimationStarted() =
runTest(UnconfinedTestDispatcher()) {
val value by collectLastValue(underTest.revealAmount)
underTest.startRevealAmountAnimator(true)
assertEquals(0.0f, value)
animatorTestRule.advanceTimeBy(500L)
assertEquals(1.0f, value)
}
@Test
@TestableLooper.RunWithLooper(setAsMainLooper = true)
fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
runTest(UnconfinedTestDispatcher()) {
val value by collectLastValue(underTest.revealAmount)
underTest.startRevealAmountAnimator(false)
assertEquals(1.0f, value)
animatorTestRule.advanceTimeBy(500L)
assertEquals(0.0f, value)
}
/**

View File

@@ -27,27 +27,37 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@SmallTest
@RoboPilotTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class LightRevealScrimInteractorTest : SysuiTestCase() {
private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
@Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
private val testScope = TestScope()
private val keyguardTransitionInteractor =
KeyguardTransitionInteractorFactory.create(
scope = TestScope().backgroundScope,
scope = testScope.backgroundScope,
repository = fakeKeyguardTransitionRepository,
)
.keyguardTransitionInteractor
@@ -69,9 +79,9 @@ class LightRevealScrimInteractorTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
underTest =
LightRevealScrimInteractor(
fakeKeyguardTransitionRepository,
keyguardTransitionInteractor,
fakeLightRevealScrimRepository
fakeLightRevealScrimRepository,
testScope.backgroundScope
)
}
@@ -110,52 +120,36 @@ class LightRevealScrimInteractorTest : SysuiTestCase() {
}
@Test
fun revealAmount_invertedWhenAppropriate() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<Float>()
val job = underTest.revealAmount.onEach(values::add).launchIn(this)
fun lightRevealEffect_startsAnimationOnlyForDifferentStateTargets() =
testScope.runTest {
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
from = KeyguardState.AOD,
to = KeyguardState.LOCKSCREEN,
value = 0.3f
transitionState = TransitionState.STARTED,
from = KeyguardState.OFF,
to = KeyguardState.OFF
)
)
assertEquals(values, listOf(0.3f))
runCurrent()
verify(fakeLightRevealScrimRepository, never()).startRevealAmountAnimator(anyBoolean())
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
transitionState = TransitionState.STARTED,
from = KeyguardState.DOZING,
to = KeyguardState.LOCKSCREEN
)
)
runCurrent()
verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(true)
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
transitionState = TransitionState.STARTED,
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.AOD,
value = 0.3f
to = KeyguardState.DOZING
)
)
assertEquals(values, listOf(0.3f, 0.7f))
job.cancel()
}
@Test
fun revealAmount_ignoresTransitionsThatDoNotAffectRevealAmount() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<Float>()
val job = underTest.revealAmount.onEach(values::add).launchIn(this)
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.AOD, value = 0.3f)
)
assertEquals(values, emptyList<Float>())
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f)
)
assertEquals(values, emptyList<Float>())
job.cancel()
runCurrent()
verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false)
}
}

View File

@@ -29,6 +29,7 @@ import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.DozeInteractor
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.power.data.repository.FakePowerRepository
@@ -73,6 +74,8 @@ class PulsingGestureListenerTest : SysuiTestCase() {
@Mock
private lateinit var userTracker: UserTracker
@Mock
private lateinit var dozeInteractor: DozeInteractor
@Mock
private lateinit var screenOffAnimationController: ScreenOffAnimationController
private lateinit var powerRepository: FakePowerRepository
@@ -98,6 +101,7 @@ class PulsingGestureListenerTest : SysuiTestCase() {
ambientDisplayConfiguration,
statusBarStateController,
shadeLogger,
dozeInteractor,
userTracker,
tunerService,
dumpManager

View File

@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository
import com.android.systemui.statusbar.LightRevealEffect
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
/** Fake implementation of [LightRevealScrimRepository] */
@@ -30,4 +31,15 @@ class FakeLightRevealScrimRepository : LightRevealScrimRepository {
fun setRevealEffect(effect: LightRevealEffect) {
_revealEffect.tryEmit(effect)
}
private val _revealAmount: MutableStateFlow<Float> = MutableStateFlow(0.0f)
override val revealAmount: Flow<Float> = _revealAmount
override fun startRevealAmountAnimator(reveal: Boolean) {
if (reveal) {
_revealAmount.value = 1.0f
} else {
_revealAmount.value = 0.0f
}
}
}