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:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user