From 3871e2d3276eb5cd956e09540a9e8cd78cb46e53 Mon Sep 17 00:00:00 2001 From: Andreas Miko Date: Thu, 6 Jul 2023 20:03:48 +0200 Subject: [PATCH] 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 --- .../repository/LightRevealScrimRepository.kt | 39 +++- .../interactor/LightRevealScrimInteractor.kt | 42 ++-- .../keyguard/shared/model/WakefulnessModel.kt | 35 +-- .../KeyguardIndicationAreaViewModel.kt | 6 +- .../ui/viewmodel/LightRevealScrimViewModel.kt | 2 + .../systemui/shade/PulsingGestureListener.kt | 4 + .../LightRevealScrimRepositoryTest.kt | 215 ++++++++++-------- .../LightRevealScrimInteractorTest.kt | 76 +++---- .../shade/PulsingGestureListenerTest.kt | 4 + .../FakeLightRevealScrimRepository.kt | 12 + 10 files changed, 257 insertions(+), 178 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index 482e9a3d09d76..6a2511fdb90e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -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 + + val revealAmount: Flow + + 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 = - 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 = 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( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 833eda77108d9..4244e5565a4b5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -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 = - 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) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt index cfd9e0866c067..62f43ed82a968 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index 389cf76c47acc..f1ceaaa391f51 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -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, ) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt index a46d441613acc..82f40bf3a16aa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt @@ -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 = interactor.lightRevealEffect val revealAmount: Flow = interactor.revealAmount diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index ee4e98e094fcd..fe4832f0895b7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt index f9745779ecfb5..ec30732dda232 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt @@ -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() - val job = launch { underTest.revealEffect.collect { values.add(it) } } + fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest { + val values = mutableListOf() + 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) } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt index 6e7ba6dd1ecb5..906d948591408 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt @@ -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() - 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() - val job = underTest.revealAmount.onEach(values::add).launchIn(this) - - fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.AOD, value = 0.3f) - ) - - assertEquals(values, emptyList()) - - fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f) - ) - - assertEquals(values, emptyList()) - - job.cancel() + runCurrent() + verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt index 77a22ac9b092d..29bc64e6249d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt @@ -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 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt index 7c22604dc5465..b24b95e0d3d77 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt @@ -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 = MutableStateFlow(0.0f) + override val revealAmount: Flow = _revealAmount + + override fun startRevealAmountAnimator(reveal: Boolean) { + if (reveal) { + _revealAmount.value = 1.0f + } else { + _revealAmount.value = 0.0f + } + } }