From 206f2e7894b5a911601bb768d0b6925ae0a138c5 Mon Sep 17 00:00:00 2001 From: Josh Tsuji Date: Wed, 30 Nov 2022 14:04:59 -0500 Subject: [PATCH] Migrate LightRevealScrim and all dependencies to flows. Bug: 195430376 Test: atest LightRevealScrimRepositoryTest Test: atest LightRevealScrimInteractorTest Change-Id: I5ddede5301b4f3cf92d678e9276785823ae29d25 --- .../systemui/biometrics/AuthController.java | 26 ++- .../biometrics/AuthRippleController.kt | 26 ++- .../src/com/android/systemui/flags/Flags.kt | 7 + .../data/repository/KeyguardRepository.kt | 198 +++++++++++++----- .../repository/KeyguardRepositoryModule.kt | 3 + .../repository/LightRevealScrimRepository.kt | 149 +++++++++++++ .../AodLockscreenTransitionInteractor.kt | 1 - .../DreamingToAodTransitionInteractor.kt | 2 +- .../interactor/GoneAodTransitionInteractor.kt | 7 +- .../domain/interactor/KeyguardInteractor.kt | 12 +- .../KeyguardTransitionAuditLogger.kt | 2 +- .../interactor/LightRevealScrimInteractor.kt | 95 +++++++++ .../LockscreenBouncerTransitionInteractor.kt | 45 ++-- .../shared/model/BiometricUnlockSource.kt | 39 ++++ .../keyguard/shared/model/WakeSleepReason.kt | 44 ++++ .../keyguard/shared/model/WakefulnessModel.kt | 34 +-- .../keyguard/shared/model/WakefulnessState.kt | 44 ++++ .../ui/binder/LightRevealScrimViewBinder.kt | 43 ++++ .../ui/viewmodel/LightRevealScrimViewModel.kt | 31 +++ .../phone/BiometricUnlockController.java | 2 +- .../statusbar/phone/CentralSurfacesImpl.java | 29 ++- .../biometrics/AuthRippleControllerTest.kt | 3 + .../repository/KeyguardRepositoryImplTest.kt | 160 ++++++++++++-- .../LightRevealScrimRepositoryTest.kt | 167 +++++++++++++++ .../LightRevealScrimInteractorTest.kt | 154 ++++++++++++++ .../phone/CentralSurfacesImplTest.java | 5 +- .../data/repository/FakeKeyguardRepository.kt | 43 +++- .../FakeLightRevealScrimRepository.kt | 33 +++ 28 files changed, 1273 insertions(+), 131 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockSource.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessState.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index eb974dd909be3..092339a74e3f3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -598,6 +598,10 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, getFingerprintSensorLocationInNaturalOrientation(), mCachedDisplayInfo); } + + for (final Callback cb : mCallbacks) { + cb.onFingerprintLocationChanged(); + } } /** @@ -619,6 +623,10 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mCachedDisplayInfo ); } + + for (final Callback cb : mCallbacks) { + cb.onFaceSensorLocationChanged(); + } } /** @@ -1265,8 +1273,24 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, default void onBiometricPromptDismissed() {} /** - * The location in pixels can change due to resolution changes. + * Called when the location of the fingerprint sensor changes. The location in pixels can + * change due to resolution changes. + */ + default void onFingerprintLocationChanged() {} + + /** + * Called when the location of the under display fingerprint sensor changes. The location in + * pixels can change due to resolution changes. + * + * On devices with UDFPS, this is always called alongside + * {@link #onFingerprintLocationChanged}. */ default void onUdfpsLocationChanged() {} + + /** + * Called when the location of the face unlock sensor (typically the front facing camera) + * changes. The location in pixels can change due to resolution changes. + */ + default void onFaceSensorLocationChanged() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 0c1cb9233c892..d68fcd0a59226 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -29,6 +29,8 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.animation.Interpolators +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.CircleReveal @@ -71,7 +73,8 @@ class AuthRippleController @Inject constructor( private val biometricUnlockController: BiometricUnlockController, private val udfpsControllerProvider: Provider, private val statusBarStateController: StatusBarStateController, - rippleView: AuthRippleView? + private val featureFlags: FeatureFlags, + rippleView: AuthRippleView? ) : ViewController(rippleView), KeyguardStateController.Callback, WakefulnessLifecycle.Observer { @@ -159,12 +162,17 @@ class AuthRippleController @Inject constructor( private fun showUnlockedRipple() { notificationShadeWindowController.setForcePluginOpen(true, this) - val lightRevealScrim = centralSurfaces.lightRevealScrim - if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) { - circleReveal?.let { - lightRevealScrim?.revealAmount = 0f - lightRevealScrim?.revealEffect = it - startLightRevealScrimOnKeyguardFadingAway = true + + // This code path is not used if the KeyguardTransitionRepository is managing the light + // reveal scrim. + if (!featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { + val lightRevealScrim = centralSurfaces.lightRevealScrim + if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) { + circleReveal?.let { + lightRevealScrim?.revealAmount = 0f + lightRevealScrim?.revealEffect = it + startLightRevealScrimOnKeyguardFadingAway = true + } } } @@ -177,6 +185,10 @@ class AuthRippleController @Inject constructor( } override fun onKeyguardFadingAwayChanged() { + if (featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { + return + } + if (keyguardStateController.isKeyguardFadingAway) { val lightRevealScrim = centralSurfaces.lightRevealScrim if (startLightRevealScrimOnKeyguardFadingAway && lightRevealScrim != null) { diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index db19749d06dec..a118b78fc50a5 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -164,6 +164,13 @@ object Flags { // TODO(b/256513609): Tracking Bug @JvmField val ACTIVE_UNLOCK_CHIPBAR = releasedFlag(217, "active_unlock_chipbar") + /** + * Migrates control of the LightRevealScrim's reveal effect and amount from legacy code to the + * new KeyguardTransitionRepository. + */ + @JvmField + val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = true) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 796f2b44effc6..148792ba779b3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -16,8 +16,11 @@ package com.android.systemui.keyguard.data.repository +import android.graphics.Point +import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.biometrics.AuthController import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.Position @@ -27,8 +30,8 @@ import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.StatusBarState @@ -88,8 +91,8 @@ interface KeyguardRepository { * enter to conserve battery when the device is locked and inactive. * * Note that it is possible for the system to be transitioning into doze while this flow still - * returns `false`. In order to account for that, observers should also use the [dozeAmount] - * flow to check if it's greater than `0` + * returns `false`. In order to account for that, observers should also use the + * [linearDozeAmount] flow to check if it's greater than `0` */ val isDozing: Flow @@ -111,7 +114,7 @@ interface KeyguardRepository { * happens during an animation/transition into doze mode. An observer would be wise to account * for both flows if needed. */ - val dozeAmount: Flow + val linearDozeAmount: Flow /** Doze state information, as it transitions */ val dozeTransitionModel: Flow @@ -120,11 +123,20 @@ interface KeyguardRepository { val statusBarState: Flow /** Observable for device wake/sleep state */ - val wakefulnessState: Flow + val wakefulness: Flow /** Observable for biometric unlock modes */ val biometricUnlockState: Flow + /** Approximate location on the screen of the fingerprint sensor. */ + val fingerprintSensorLocation: Flow + + /** Approximate location on the screen of the face unlock sensor/front facing camera. */ + val faceSensorLocation: Flow + + /** Source of the most recent biometric unlock, such as fingerprint or face. */ + val biometricUnlockSource: Flow + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -163,6 +175,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, + private val authController: AuthController, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -281,11 +294,11 @@ constructor( } .distinctUntilChanged() - override val dozeAmount: Flow = conflatedCallbackFlow { + override val linearDozeAmount: Flow = conflatedCallbackFlow { val callback = object : StatusBarStateController.StateListener { override fun onDozeAmountChanged(linear: Float, eased: Float) { - trySendWithFailureLogging(eased, TAG, "updated dozeAmount") + trySendWithFailureLogging(linear, TAG, "updated dozeAmount") } } @@ -348,58 +361,139 @@ constructor( awaitClose { statusBarStateController.removeCallback(callback) } } - override val wakefulnessState: Flow = conflatedCallbackFlow { - val callback = - object : WakefulnessLifecycle.Observer { - override fun onStartedWakingUp() { - trySendWithFailureLogging( - WakefulnessModel.STARTING_TO_WAKE, - TAG, - "Wakefulness: starting to wake" - ) - } - override fun onFinishedWakingUp() { - trySendWithFailureLogging(WakefulnessModel.AWAKE, TAG, "Wakefulness: awake") - } - override fun onStartedGoingToSleep() { - trySendWithFailureLogging( - WakefulnessModel.STARTING_TO_SLEEP, - TAG, - "Wakefulness: starting to sleep" - ) - } - override fun onFinishedGoingToSleep() { - trySendWithFailureLogging(WakefulnessModel.ASLEEP, TAG, "Wakefulness: asleep") - } - } - wakefulnessLifecycle.addObserver(callback) - trySendWithFailureLogging( - wakefulnessIntToObject(wakefulnessLifecycle.getWakefulness()), - TAG, - "initial wakefulness state" - ) - - awaitClose { wakefulnessLifecycle.removeObserver(callback) } - } - override val biometricUnlockState: Flow = conflatedCallbackFlow { + fun dispatchUpdate() { + trySendWithFailureLogging( + biometricModeIntToObject(biometricUnlockController.mode), + TAG, + "biometric mode" + ) + } + val callback = object : BiometricUnlockController.BiometricModeListener { override fun onModeChanged(@WakeAndUnlockMode mode: Int) { - trySendWithFailureLogging(biometricModeIntToObject(mode), TAG, "biometric mode") + dispatchUpdate() + } + + override fun onResetMode() { + dispatchUpdate() } } biometricUnlockController.addBiometricModeListener(callback) - trySendWithFailureLogging( - biometricModeIntToObject(biometricUnlockController.getMode()), - TAG, - "initial biometric mode" - ) + dispatchUpdate() awaitClose { biometricUnlockController.removeBiometricModeListener(callback) } } + override val wakefulness: Flow = conflatedCallbackFlow { + val observer = + object : WakefulnessLifecycle.Observer { + override fun onStartedWakingUp() { + dispatchNewState() + } + + override fun onFinishedWakingUp() { + dispatchNewState() + } + + override fun onPostFinishedWakingUp() { + dispatchNewState() + } + + override fun onStartedGoingToSleep() { + dispatchNewState() + } + + override fun onFinishedGoingToSleep() { + dispatchNewState() + } + + private fun dispatchNewState() { + trySendWithFailureLogging( + WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle), + TAG, + "updated wakefulness state" + ) + } + } + + wakefulnessLifecycle.addObserver(observer) + trySendWithFailureLogging( + WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle), + TAG, + "initial wakefulness state" + ) + + awaitClose { wakefulnessLifecycle.removeObserver(observer) } + } + + override val fingerprintSensorLocation: Flow = conflatedCallbackFlow { + fun sendFpLocation() { + trySendWithFailureLogging( + authController.fingerprintSensorLocation, + TAG, + "AuthController.Callback#onFingerprintLocationChanged" + ) + } + + val callback = + object : AuthController.Callback { + override fun onFingerprintLocationChanged() { + sendFpLocation() + } + } + + authController.addCallback(callback) + sendFpLocation() + + awaitClose { authController.removeCallback(callback) } + } + + override val faceSensorLocation: Flow = conflatedCallbackFlow { + fun sendSensorLocation() { + trySendWithFailureLogging( + authController.faceSensorLocation, + TAG, + "AuthController.Callback#onFingerprintLocationChanged" + ) + } + + val callback = + object : AuthController.Callback { + override fun onFaceSensorLocationChanged() { + sendSensorLocation() + } + } + + authController.addCallback(callback) + sendSensorLocation() + + awaitClose { authController.removeCallback(callback) } + } + + override val biometricUnlockSource: Flow = conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onBiometricAuthenticated( + userId: Int, + biometricSourceType: BiometricSourceType?, + isStrongBiometric: Boolean + ) { + trySendWithFailureLogging( + BiometricUnlockSource.fromBiometricSourceType(biometricSourceType), + TAG, + "onBiometricAuthenticated" + ) + } + } + + keyguardUpdateMonitor.registerCallback(callback) + trySendWithFailureLogging(null, TAG, "initial value") + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -423,16 +517,6 @@ constructor( } } - private fun wakefulnessIntToObject(@Wakefulness value: Int): WakefulnessModel { - return when (value) { - 0 -> WakefulnessModel.ASLEEP - 1 -> WakefulnessModel.STARTING_TO_WAKE - 2 -> WakefulnessModel.AWAKE - 3 -> WakefulnessModel.STARTING_TO_SLEEP - else -> throw IllegalArgumentException("Invalid Wakefulness value: $value") - } - } - private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockModel { return when (value) { 0 -> BiometricUnlockModel.NONE diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt index 0c725208e22dc..26f853f3ad1c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -27,4 +27,7 @@ interface KeyguardRepositoryModule { fun keyguardTransitionRepository( impl: KeyguardTransitionRepositoryImpl ): KeyguardTransitionRepository + + @Binds + fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository } 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 new file mode 100644 index 0000000000000..a17481a9978b7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -0,0 +1,149 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.data.repository + +import android.content.Context +import android.graphics.Point +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 +import com.android.systemui.statusbar.CircleReveal +import com.android.systemui.statusbar.LiftReveal +import com.android.systemui.statusbar.LightRevealEffect +import com.android.systemui.statusbar.PowerButtonReveal +import javax.inject.Inject +import kotlin.math.max +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +val DEFAULT_REVEAL_EFFECT = LiftReveal + +/** + * Encapsulates state relevant to the light reveal scrim, the view used to reveal/hide screen + * contents during transitions between AOD and lockscreen/unlocked. + */ +interface LightRevealScrimRepository { + + /** + * The reveal effect that should be used for the next lock/unlock. We switch between either the + * biometric unlock effect (if wake and unlocking) or the non-biometric effect, and position it + * at the current screen position of the appropriate sensor. + */ + val revealEffect: Flow +} + +@SysUISingleton +class LightRevealScrimRepositoryImpl +@Inject +constructor( + keyguardRepository: KeyguardRepository, + val context: Context, +) : LightRevealScrimRepository { + + /** The reveal effect used if the device was locked/unlocked via the power button. */ + private val powerButtonReveal = + PowerButtonReveal( + context.resources + .getDimensionPixelSize(R.dimen.physical_power_button_center_screen_location_y) + .toFloat() + ) + + /** + * Reveal effect to use for a fingerprint unlock. This is reconstructed if the fingerprint + * sensor location on the screen (in pixels) changes due to configuration changes. + */ + private val fingerprintRevealEffect: Flow = + keyguardRepository.fingerprintSensorLocation.map { + it?.let { constructCircleRevealFromPoint(it) } + } + + /** + * Reveal effect to use for a face unlock. This is reconstructed if the face sensor/front camera + * location on the screen (in pixels) changes due to configuration changes. + */ + private val faceRevealEffect: Flow = + keyguardRepository.faceSensorLocation.map { it?.let { constructCircleRevealFromPoint(it) } } + + /** + * The reveal effect we'll use for the next biometric unlock animation. We switch between the + * fingerprint/face unlock effect flows depending on the biometric unlock source. + */ + private val biometricRevealEffect: Flow = + keyguardRepository.biometricUnlockSource.flatMapLatest { source -> + when (source) { + BiometricUnlockSource.FINGERPRINT_SENSOR -> fingerprintRevealEffect + BiometricUnlockSource.FACE_SENSOR -> faceRevealEffect + else -> flowOf(null) + } + } + + /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */ + private val nonBiometricRevealEffect: Flow = + keyguardRepository.wakefulness.map { wakefulnessModel -> + val wakingUpFromPowerButton = + wakefulnessModel.isWakingUpOrAwake && + wakefulnessModel.lastWakeReason == WakeSleepReason.POWER_BUTTON + val sleepingFromPowerButton = + !wakefulnessModel.isWakingUpOrAwake && + wakefulnessModel.lastSleepReason == WakeSleepReason.POWER_BUTTON + + if (wakingUpFromPowerButton || sleepingFromPowerButton) { + powerButtonReveal + } else { + LiftReveal + } + } + + override val revealEffect = + combine( + keyguardRepository.biometricUnlockState, + biometricRevealEffect, + nonBiometricRevealEffect + ) { biometricUnlockState, biometricReveal, nonBiometricReveal -> + + // Use the biometric reveal for any flavor of wake and unlocking. + when (biometricUnlockState) { + BiometricUnlockModel.WAKE_AND_UNLOCK, + BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING, + BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM -> biometricReveal + else -> nonBiometricReveal + } + ?: DEFAULT_REVEAL_EFFECT + } + .distinctUntilChanged() + + private fun constructCircleRevealFromPoint(point: Point): LightRevealEffect { + return with(point) { + CircleReveal( + x, + y, + startRadius = 0, + endRadius = + max(max(x, context.display.width - x), max(y, context.display.height - y)), + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt index 2dbacd5d3c693..9b193533805ef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -27,7 +27,6 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt index 9e2b7241ade22..e34981eabcac0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingToAodTransitionInteractor.kt @@ -42,7 +42,7 @@ constructor( override fun start() { scope.launch { - keyguardInteractor.wakefulnessState + keyguardInteractor.wakefulnessModel .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) .collect { pair -> val (wakefulnessState, keyguardState) = pair diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt index 0e2a54c57bdb4..483041a1b2365 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt @@ -23,11 +23,10 @@ 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.TransitionInfo -import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @SysUISingleton @@ -42,13 +41,13 @@ constructor( override fun start() { scope.launch { - keyguardInteractor.wakefulnessState + keyguardInteractor.wakefulnessModel .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) .collect { pair -> val (wakefulnessState, keyguardState) = pair if ( keyguardState == KeyguardState.GONE && - wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ) { keyguardTransitionRepository.startTransition( TransitionInfo( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 7cfd117d8eb43..6912e1d3558e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor +import android.graphics.Point import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel @@ -41,7 +42,7 @@ constructor( * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at * all. */ - val dozeAmount: Flow = repository.dozeAmount + val dozeAmount: Flow = repository.linearDozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow = repository.isDozing /** Doze transition information. */ @@ -58,7 +59,7 @@ constructor( /** Whether the bouncer is showing or not. */ val isBouncerShowing: Flow = repository.isBouncerShowing /** The device wake/sleep state */ - val wakefulnessState: Flow = repository.wakefulnessState + val wakefulnessModel: Flow = repository.wakefulness /** Observable for the [StatusBarState] */ val statusBarState: Flow = repository.statusBarState /** @@ -67,10 +68,15 @@ constructor( */ val biometricUnlockState: Flow = repository.biometricUnlockState + /** The approximate location on the screen of the fingerprint sensor, if one is available. */ + val fingerprintSensorLocation: Flow = repository.fingerprintSensorLocation + + /** The approximate location on the screen of the face unlock sensor, if one is available. */ + val faceSensorLocation: Flow = repository.faceSensorLocation + fun dozeTransitionTo(state: DozeStateModel): Flow { return dozeTransitionModel.filter { it.to == state } } - fun isKeyguardShowing(): Boolean { return repository.isKeyguardShowing() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index 58a8093d49d23..e30e7f6ece110 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -37,7 +37,7 @@ constructor( fun start() { scope.launch { - keyguardInteractor.wakefulnessState.collect { logger.v("WakefulnessState", it) } + keyguardInteractor.wakefulnessModel.collect { logger.v("WakefulnessModel", it) } } scope.launch { 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 new file mode 100644 index 0000000000000..6e25200bc2f64 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -0,0 +1,95 @@ +/* + * 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 com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +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.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map + +@ExperimentalCoroutinesApi +@SysUISingleton +class LightRevealScrimInteractor +@Inject +constructor( + transitionRepository: KeyguardTransitionRepository, + transitionInteractor: KeyguardTransitionInteractor, + lightRevealScrimRepository: LightRevealScrimRepository, +) { + + /** + * Whenever a keyguard transition starts, sample the latest reveal effect from the repository + * and use that for the starting transition. + * + * We can't simply use the nextRevealEffect since the effect may change midway through a + * transition, but we don't want to change effects part way through. For example, if we're using + * a CircleReveal to animate a biometric unlock, but the biometric unlock mode changes to NONE + * from WAKE_AND_UNLOCK before the unlock animation ends, we don't want to end up switching to a + * LiftReveal. + */ + val lightRevealEffect: Flow = + transitionInteractor.startedKeyguardTransitionStep.sample( + 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 } + + companion object { + + /** + * 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 { + return willBeRevealedInState(transition.from) != willBeRevealedInState(transition.to) + } + + /** + * Whether the light reveal scrim will be fully revealed (revealAmount = 1.0f) in the given + * state after the transition is complete. If false, scrim will be fully hidden. + */ + fun willBeRevealedInState(state: KeyguardState): Boolean { + return when (state) { + KeyguardState.OFF -> false + KeyguardState.DOZING -> false + KeyguardState.AOD -> false + KeyguardState.DREAMING -> true + KeyguardState.BOUNCER -> true + KeyguardState.LOCKSCREEN -> true + KeyguardState.GONE -> true + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt index 3bb8241257cc0..3218f9699f350 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -25,7 +25,7 @@ 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.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -58,22 +58,26 @@ constructor( keyguardInteractor.isBouncerShowing .sample( combine( - keyguardInteractor.wakefulnessState, + keyguardInteractor.wakefulnessModel, keyguardTransitionInteractor.startedKeyguardTransitionStep, - ) { a, b -> - Pair(a, b) - }, - { a, bc -> Triple(a, bc.first, bc.second) } - ) - .collect { triple -> - val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple + ) { wakefulnessModel, transitionStep -> + Pair(wakefulnessModel, transitionStep) + } + ) { bouncerShowing, wakefulnessAndTransition -> + Triple( + bouncerShowing, + wakefulnessAndTransition.first, + wakefulnessAndTransition.second + ) + } + .collect { (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) -> if ( !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER ) { val to = if ( - wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP || - wakefulnessState == WakefulnessModel.ASLEEP + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP || + wakefulnessState.state == WakefulnessState.ASLEEP ) { KeyguardState.AOD } else { @@ -100,14 +104,17 @@ constructor( combine( keyguardTransitionInteractor.finishedKeyguardState, keyguardInteractor.statusBarState, - ) { a, b -> - Pair(a, b) - }, - { a, bc -> Triple(a, bc.first, bc.second) } - ) - .collect { triple -> - val (shadeModel, keyguardState, statusBarState) = triple - + ) { finishedKeyguardState, statusBarState -> + Pair(finishedKeyguardState, statusBarState) + } + ) { shadeModel, keyguardStateAndStatusBarState -> + Triple( + shadeModel, + keyguardStateAndStatusBarState.first, + keyguardStateAndStatusBarState.second + ) + } + .collect { (shadeModel, keyguardState, statusBarState) -> val id = transitionId if (id != null) { // An existing `id` means a transition is started, and calls to diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockSource.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockSource.kt new file mode 100644 index 0000000000000..b403416c572c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockSource.kt @@ -0,0 +1,39 @@ +/* + * 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.shared.model + +import android.hardware.biometrics.BiometricSourceType + +/** Biometric unlock sensor sources, which we use to play sensor-specific animations. */ +enum class BiometricUnlockSource { + /** The unlock was initiated by a fingerprint sensor authentication. */ + FINGERPRINT_SENSOR, + + /** The unlock was initiated by the front-facing camera or a nearby sensor. */ + FACE_SENSOR; + + companion object { + fun fromBiometricSourceType(type: BiometricSourceType?): BiometricUnlockSource? { + return when (type) { + BiometricSourceType.FINGERPRINT -> FINGERPRINT_SENSOR + BiometricSourceType.FACE -> FACE_SENSOR + BiometricSourceType.IRIS -> FACE_SENSOR + else -> null + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt new file mode 100644 index 0000000000000..b32597d5cff0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakeSleepReason.kt @@ -0,0 +1,44 @@ +/* + * 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.shared.model + +import android.os.PowerManager + +/** The reason we're waking up or going to sleep, such as pressing the power button. */ +enum class WakeSleepReason { + /** The physical power button was pressed to wake up or sleep the device. */ + POWER_BUTTON, + + /** Something else happened to wake up or sleep the device. */ + OTHER; + + companion object { + fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { + return when (reason) { + PowerManager.WAKE_REASON_POWER_BUTTON -> POWER_BUTTON + else -> OTHER + } + } + + fun fromPowerManagerSleepReason(reason: Int): WakeSleepReason { + return when (reason) { + PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON -> POWER_BUTTON + else -> OTHER + } + } + } +} 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 92040f4f03481..03dee0045c108 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 @@ -15,24 +15,34 @@ */ package com.android.systemui.keyguard.shared.model -/** Model device wakefulness states. */ -enum class WakefulnessModel { - /** The device is asleep and not interactive. */ - ASLEEP, - /** Received a signal that the device is beginning to wake up. */ - STARTING_TO_WAKE, - /** Device is now fully awake and interactive. */ - AWAKE, - /** Signal that the device is now going to sleep. */ - STARTING_TO_SLEEP; +import com.android.systemui.keyguard.WakefulnessLifecycle +/** Model device wakefulness states. */ +data class WakefulnessModel( + val state: WakefulnessState, + val isWakingUpOrAwake: Boolean, + val lastWakeReason: WakeSleepReason, + val lastSleepReason: WakeSleepReason, +) { companion object { fun isSleepingOrStartingToSleep(model: WakefulnessModel): Boolean { - return model == ASLEEP || model == STARTING_TO_SLEEP + return model.state == WakefulnessState.ASLEEP || + model.state == WakefulnessState.STARTING_TO_SLEEP } fun isWakingOrStartingToWake(model: WakefulnessModel): Boolean { - return model == AWAKE || model == STARTING_TO_WAKE + return model.state == WakefulnessState.AWAKE || + model.state == WakefulnessState.STARTING_TO_WAKE + } + + fun fromWakefulnessLifecycle(wakefulnessLifecycle: WakefulnessLifecycle): WakefulnessModel { + return WakefulnessModel( + WakefulnessState.fromWakefulnessLifecycleInt(wakefulnessLifecycle.wakefulness), + wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_WAKING || + wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_AWAKE, + WakeSleepReason.fromPowerManagerWakeReason(wakefulnessLifecycle.lastWakeReason), + WakeSleepReason.fromPowerManagerSleepReason(wakefulnessLifecycle.lastSleepReason), + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessState.kt new file mode 100644 index 0000000000000..6791d88f16d13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessState.kt @@ -0,0 +1,44 @@ +/* + * 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.shared.model + +import com.android.systemui.keyguard.WakefulnessLifecycle + +enum class WakefulnessState { + /** The device is asleep and not interactive. */ + ASLEEP, + /** Received a signal that the device is beginning to wake up. */ + STARTING_TO_WAKE, + /** Device is now fully awake and interactive. */ + AWAKE, + /** Signal that the device is now going to sleep. */ + STARTING_TO_SLEEP; + + companion object { + fun fromWakefulnessLifecycleInt( + @WakefulnessLifecycle.Wakefulness value: Int + ): WakefulnessState { + return when (value) { + WakefulnessLifecycle.WAKEFULNESS_ASLEEP -> ASLEEP + WakefulnessLifecycle.WAKEFULNESS_WAKING -> STARTING_TO_WAKE + WakefulnessLifecycle.WAKEFULNESS_AWAKE -> AWAKE + WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP -> STARTING_TO_SLEEP + else -> throw IllegalArgumentException("Invalid Wakefulness value: $value") + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt new file mode 100644 index 0000000000000..f1da8826a22ce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt @@ -0,0 +1,43 @@ +/* + * 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.ui.binder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.LightRevealScrim +import kotlinx.coroutines.launch + +object LightRevealScrimViewBinder { + @JvmStatic + fun bind(revealScrim: LightRevealScrim, viewModel: LightRevealScrimViewModel) { + revealScrim.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + viewModel.revealAmount.collect { amount -> revealScrim.revealAmount = amount } + } + + launch { + viewModel.lightRevealEffect.collect { effect -> + revealScrim.revealEffect = effect + } + } + } + } + } +} 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 new file mode 100644 index 0000000000000..a46d441613acc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt @@ -0,0 +1,31 @@ +/* + * 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.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.LightRevealScrimInteractor +import com.android.systemui.statusbar.LightRevealEffect +import javax.inject.Inject +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. + */ +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/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 679bcea00f745..556431114ff15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -514,7 +514,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp && mPendingAuthenticated.userId == KeyguardUpdateMonitor.getCurrentUser(); } - public int getMode() { + public @WakeAndUnlockMode int getMode() { return mMode; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 00a9916f5ac15..2d48ffcf98103 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -160,6 +160,8 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder; +import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.plugins.DarkIconDispatcher; @@ -491,6 +493,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final OngoingCallController mOngoingCallController; private final StatusBarSignalPolicy mStatusBarSignalPolicy; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; + private final Lazy mLightRevealScrimViewModelLazy; /** Controller for the Shade. */ @VisibleForTesting @@ -757,7 +760,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { DeviceStateManager deviceStateManager, WiredChargingRippleController wiredChargingRippleController, IDreamManager dreamManager, - Lazy cameraLauncherLazy) { + Lazy cameraLauncherLazy, + Lazy lightRevealScrimViewModelLazy) { mContext = context; mNotificationsController = notificationsController; mFragmentService = fragmentService; @@ -871,6 +875,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { deviceStateManager.registerCallback(mMainExecutor, new FoldStateListener(mContext, this::onFoldedStateChanged)); wiredChargingRippleController.registerCallbacks(); + + mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy; } @Override @@ -1000,6 +1006,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onKeyguardGoingAwayChanged() { + if (mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { + // This code path is not used if the KeyguardTransitionRepository is managing + // the lightreveal scrim. + return; + } + // The light reveal scrim should always be fully revealed by the time the keyguard // is done going away. Double check that this is true. if (!mKeyguardStateController.isKeyguardGoingAway()) { @@ -1236,6 +1248,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront); mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim); + + if (mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { + LightRevealScrimViewBinder.bind( + mLightRevealScrim, mLightRevealScrimViewModelLazy.get()); + } + mLightRevealScrim.setScrimOpaqueChangedListener((opaque) -> { Runnable updateOpaqueness = () -> { mNotificationShadeWindowController.setLightRevealScrimOpaque( @@ -3308,6 +3326,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return; } + if (mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { + return; + } + final boolean wakingUpFromPowerButton = wakingUp && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal) && mWakefulnessLifecycle.getLastWakeReason() @@ -4072,7 +4094,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return; } - mLightRevealScrim.setAlpha(mScrimController.getState().getMaxLightRevealScrimAlpha()); + if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) { + mLightRevealScrim.setAlpha(mScrimController.getState().getMaxLightRevealScrimAlpha()); + } } @Override @@ -4253,6 +4277,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onDozeAmountChanged(float linear, float eased) { if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) + && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { mLightRevealScrim.setRevealAmount(1f - linear); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index eb8c823ffe1c2..b765ab3c5eac6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -26,6 +26,7 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.LightRevealScrim @@ -77,6 +78,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var udfpsControllerProvider: Provider @Mock private lateinit var udfpsController: UdfpsController @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var lightRevealScrim: LightRevealScrim @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal @@ -106,6 +108,7 @@ class AuthRippleControllerTest : SysuiTestCase() { biometricUnlockController, udfpsControllerProvider, statusBarStateController, + featureFlags, rippleView ) controller.init() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 13fc9fcf834bf..5deac1924ab78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -16,10 +16,13 @@ package com.android.systemui.keyguard.data.repository +import android.graphics.Point +import android.hardware.biometrics.BiometricSourceType import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController import com.android.systemui.common.shared.model.Position import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine @@ -27,9 +30,11 @@ import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.KeyguardStateController @@ -57,9 +62,10 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var dozeHost: DozeHost @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var biometricUnlockController: BiometricUnlockController @Mock private lateinit var dozeTransitionListener: DozeTransitionListener + @Mock private lateinit var authController: AuthController + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: KeyguardRepositoryImpl @@ -76,6 +82,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardStateController, keyguardUpdateMonitor, dozeTransitionListener, + authController, ) } @@ -198,7 +205,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { fun dozeAmount() = runTest(UnconfinedTestDispatcher()) { val values = mutableListOf() - val job = underTest.dozeAmount.onEach(values::add).launchIn(this) + val job = underTest.linearDozeAmount.onEach(values::add).launchIn(this) val captor = argumentCaptor() verify(statusBarStateController).addCallback(captor.capture()) @@ -207,7 +214,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { captor.value.onDozeAmountChanged(0.498f, 0.5f) captor.value.onDozeAmountChanged(0.661f, 0.65f) - assertThat(values).isEqualTo(listOf(0f, 0.4f, 0.5f, 0.65f)) + assertThat(values).isEqualTo(listOf(0f, 0.433f, 0.498f, 0.661f)) job.cancel() verify(statusBarStateController).removeCallback(captor.value) @@ -217,25 +224,36 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { fun wakefulness() = runTest(UnconfinedTestDispatcher()) { val values = mutableListOf() - val job = underTest.wakefulnessState.onEach(values::add).launchIn(this) + val job = underTest.wakefulness.onEach(values::add).launchIn(this) val captor = argumentCaptor() verify(wakefulnessLifecycle).addObserver(captor.capture()) + whenever(wakefulnessLifecycle.wakefulness) + .thenReturn(WakefulnessLifecycle.WAKEFULNESS_WAKING) captor.value.onStartedWakingUp() + + whenever(wakefulnessLifecycle.wakefulness) + .thenReturn(WakefulnessLifecycle.WAKEFULNESS_AWAKE) captor.value.onFinishedWakingUp() + + whenever(wakefulnessLifecycle.wakefulness) + .thenReturn(WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP) captor.value.onStartedGoingToSleep() + + whenever(wakefulnessLifecycle.wakefulness) + .thenReturn(WakefulnessLifecycle.WAKEFULNESS_ASLEEP) captor.value.onFinishedGoingToSleep() - assertThat(values) + assertThat(values.map { it.state }) .isEqualTo( listOf( // Initial value will be ASLEEP - WakefulnessModel.ASLEEP, - WakefulnessModel.STARTING_TO_WAKE, - WakefulnessModel.AWAKE, - WakefulnessModel.STARTING_TO_SLEEP, - WakefulnessModel.ASLEEP, + WakefulnessState.ASLEEP, + WakefulnessState.STARTING_TO_WAKE, + WakefulnessState.AWAKE, + WakefulnessState.STARTING_TO_SLEEP, + WakefulnessState.ASLEEP, ) ) @@ -329,14 +347,20 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { val captor = argumentCaptor() verify(biometricUnlockController).addBiometricModeListener(captor.capture()) - captor.value.onModeChanged(BiometricUnlockController.MODE_NONE) - captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK) - captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING) - captor.value.onModeChanged(BiometricUnlockController.MODE_SHOW_BOUNCER) - captor.value.onModeChanged(BiometricUnlockController.MODE_ONLY_WAKE) - captor.value.onModeChanged(BiometricUnlockController.MODE_UNLOCK_COLLAPSING) - captor.value.onModeChanged(BiometricUnlockController.MODE_DISMISS_BOUNCER) - captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM) + listOf( + BiometricUnlockController.MODE_NONE, + BiometricUnlockController.MODE_WAKE_AND_UNLOCK, + BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING, + BiometricUnlockController.MODE_SHOW_BOUNCER, + BiometricUnlockController.MODE_ONLY_WAKE, + BiometricUnlockController.MODE_UNLOCK_COLLAPSING, + BiometricUnlockController.MODE_DISMISS_BOUNCER, + BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM, + ) + .forEach { + whenever(biometricUnlockController.mode).thenReturn(it) + captor.value.onModeChanged(it) + } assertThat(values) .isEqualTo( @@ -420,4 +444,104 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { job.cancel() verify(dozeTransitionListener).removeCallback(listener) } + + @Test + fun fingerprintSensorLocation() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + val job = underTest.fingerprintSensorLocation.onEach(values::add).launchIn(this) + + val captor = argumentCaptor() + verify(authController).addCallback(captor.capture()) + + // An initial, null value should be initially emitted so that flows combined with this + // one + // emit values immediately. The sensor location is expected to be nullable, so anyone + // consuming it should handle that properly. + assertThat(values).isEqualTo(listOf(null)) + + listOf(Point(500, 500), Point(0, 0), null, Point(250, 250)) + .onEach { + whenever(authController.fingerprintSensorLocation).thenReturn(it) + captor.value.onFingerprintLocationChanged() + } + .also { dispatchedSensorLocations -> + assertThat(values).isEqualTo(listOf(null) + dispatchedSensorLocations) + } + + job.cancel() + } + + @Test + fun faceSensorLocation() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + val job = underTest.faceSensorLocation.onEach(values::add).launchIn(this) + + val captor = argumentCaptor() + verify(authController).addCallback(captor.capture()) + + // An initial, null value should be initially emitted so that flows combined with this + // one + // emit values immediately. The sensor location is expected to be nullable, so anyone + // consuming it should handle that properly. + assertThat(values).isEqualTo(listOf(null)) + + listOf( + Point(500, 500), + Point(0, 0), + null, + Point(250, 250), + ) + .onEach { + whenever(authController.faceSensorLocation).thenReturn(it) + captor.value.onFaceSensorLocationChanged() + } + .also { dispatchedSensorLocations -> + assertThat(values).isEqualTo(listOf(null) + dispatchedSensorLocations) + } + + job.cancel() + } + + @Test + fun biometricUnlockSource() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + val job = underTest.biometricUnlockSource.onEach(values::add).launchIn(this) + + val captor = argumentCaptor() + verify(keyguardUpdateMonitor).registerCallback(captor.capture()) + + // An initial, null value should be initially emitted so that flows combined with this + // one + // emit values immediately. The biometric unlock source is expected to be nullable, so + // anyone consuming it should handle that properly. + assertThat(values).isEqualTo(listOf(null)) + + listOf( + BiometricSourceType.FINGERPRINT, + BiometricSourceType.IRIS, + null, + BiometricSourceType.FACE, + BiometricSourceType.FINGERPRINT, + ) + .onEach { biometricSourceType -> + captor.value.onBiometricAuthenticated(0, biometricSourceType, false) + } + + assertThat(values) + .isEqualTo( + listOf( + null, + BiometricUnlockSource.FINGERPRINT_SENSOR, + BiometricUnlockSource.FACE_SENSOR, + null, + BiometricUnlockSource.FACE_SENSOR, + BiometricUnlockSource.FINGERPRINT_SENSOR, + ) + ) + + job.cancel() + } } 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 new file mode 100644 index 0000000000000..d2db910ad4431 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt @@ -0,0 +1,167 @@ +/* + * 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.data.repository + +import android.graphics.Point +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +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.launch +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.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class LightRevealScrimRepositoryTest : SysuiTestCase() { + private lateinit var fakeKeyguardRepository: FakeKeyguardRepository + private lateinit var underTest: LightRevealScrimRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + fakeKeyguardRepository = FakeKeyguardRepository() + underTest = LightRevealScrimRepositoryImpl(fakeKeyguardRepository, context) + } + + @Test + fun `nextRevealEffect - effect switches between default and biometric with no dupes`() = + 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 },) + + // 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 },) + + // We got a location for the face sensor, but we unlocked with fingerprint. + val faceLocation = Point(250, 0) + fakeKeyguardRepository.setFaceSensorLocation(faceLocation) + + 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 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 + }, + ) + + // 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) + + // Non-biometric unlock, we should return to the default reveal. + fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) + + runCurrent() + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + { + it is CircleReveal && + it.centerX == fingerprintLocation.x && + it.centerY == fingerprintLocation.y + }, + { it == DEFAULT_REVEAL_EFFECT }, + ) + + // 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() + } + + /** + * Asserts that the list of LightRevealEffects satisfies the list of predicates, in order, with + * no leftover elements. + */ + private fun List.assertEffectsMatchPredicates( + vararg predicates: (LightRevealEffect) -> Boolean + ) { + println(this) + assertEquals(predicates.size, this.size) + + assertFalse( + zip(predicates) { effect, predicate -> predicate(effect) }.any { matched -> !matched } + ) + } +} 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 new file mode 100644 index 0000000000000..31662145dfbe4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt @@ -0,0 +1,154 @@ +/* + * 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 androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeLightRevealScrimRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +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.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +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.junit.runners.JUnit4 +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class LightRevealScrimInteractorTest : SysuiTestCase() { + private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository() + private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository() + + private val keyguardTransitionInteractor = + KeyguardTransitionInteractor(fakeKeyguardTransitionRepository) + + private lateinit var underTest: LightRevealScrimInteractor + + private val reveal1 = + object : LightRevealEffect { + override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {} + } + + private val reveal2 = + object : LightRevealEffect { + override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {} + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + LightRevealScrimInteractor( + fakeKeyguardTransitionRepository, + keyguardTransitionInteractor, + fakeLightRevealScrimRepository + ) + } + + @Test + fun `lightRevealEffect - does not change during keyguard transition`() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + val job = underTest.lightRevealEffect.onEach(values::add).launchIn(this) + + fakeLightRevealScrimRepository.setRevealEffect(reveal1) + + // The reveal effect shouldn't emit anything until a keyguard transition starts. + assertEquals(values.size, 0) + + // Once it starts, it should emit reveal1. + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep(transitionState = TransitionState.STARTED) + ) + assertEquals(values, listOf(reveal1)) + + // Until the next transition starts, reveal2 should not be emitted. + fakeLightRevealScrimRepository.setRevealEffect(reveal2) + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep(transitionState = TransitionState.RUNNING) + ) + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep(transitionState = TransitionState.FINISHED) + ) + assertEquals(values, listOf(reveal1)) + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep(transitionState = TransitionState.STARTED) + ) + assertEquals(values, listOf(reveal1, reveal2)) + + job.cancel() + } + + @Test + fun `revealAmount - inverted when appropriate`() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() + val job = underTest.revealAmount.onEach(values::add).launchIn(this) + + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + value = 0.3f + ) + ) + + assertEquals(values, listOf(0.3f)) + + fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 0.3f + ) + ) + + assertEquals(values, listOf(0.3f, 0.7f)) + + job.cancel() + } + + @Test + fun `revealAmount - ignores transitions that do not affect reveal amount`() = + 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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index ed84e4268c903..521e518468340 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -106,6 +106,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.plugins.PluginDependencyProvider; @@ -212,6 +213,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private NotificationPanelView mNotificationPanelView; @Mock private IStatusBarService mBarService; @Mock private IDreamManager mDreamManager; + @Mock private LightRevealScrimViewModel mLightRevealScrimViewModel; @Mock private ScrimController mScrimController; @Mock private DozeScrimController mDozeScrimController; @Mock private Lazy mBiometricUnlockControllerLazy; @@ -497,7 +499,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mDeviceStateManager, mWiredChargingRippleController, mDreamManager, - mCameraLauncherLazy) { + mCameraLauncherLazy, + () -> mLightRevealScrimViewModel) { @Override protected ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 36016678fb918..5c2a915e81b6a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -17,11 +17,15 @@ package com.android.systemui.keyguard.data.repository +import android.graphics.Point import com.android.systemui.common.shared.model.Position import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.StatusBarState +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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -49,7 +53,7 @@ class FakeKeyguardRepository : KeyguardRepository { override val isDreaming: Flow = _isDreaming private val _dozeAmount = MutableStateFlow(0f) - override val dozeAmount: Flow = _dozeAmount + override val linearDozeAmount: Flow = _dozeAmount private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) override val statusBarState: Flow = _statusBarState @@ -57,8 +61,16 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeTransitionModel = MutableStateFlow(DozeTransitionModel()) override val dozeTransitionModel: Flow = _dozeTransitionModel - private val _wakefulnessState = MutableStateFlow(WakefulnessModel.ASLEEP) - override val wakefulnessState: Flow = _wakefulnessState + private val _wakefulnessModel = + MutableStateFlow( + WakefulnessModel( + WakefulnessState.ASLEEP, + false, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) + ) + override val wakefulness: Flow = _wakefulnessModel private val _isUdfpsSupported = MutableStateFlow(false) @@ -71,6 +83,15 @@ class FakeKeyguardRepository : KeyguardRepository { private val _biometricUnlockState = MutableStateFlow(BiometricUnlockModel.NONE) override val biometricUnlockState: Flow = _biometricUnlockState + private val _fingerprintSensorLocation = MutableStateFlow(null) + override val fingerprintSensorLocation: Flow = _fingerprintSensorLocation + + private val _faceSensorLocation = MutableStateFlow(null) + override val faceSensorLocation: Flow = _faceSensorLocation + + private val _biometricUnlockSource = MutableStateFlow(null) + override val biometricUnlockSource: Flow = _biometricUnlockSource + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value } @@ -99,6 +120,22 @@ class FakeKeyguardRepository : KeyguardRepository { _dozeAmount.value = dozeAmount } + fun setBiometricUnlockState(state: BiometricUnlockModel) { + _biometricUnlockState.tryEmit(state) + } + + fun setBiometricUnlockSource(source: BiometricUnlockSource?) { + _biometricUnlockSource.tryEmit(source) + } + + fun setFaceSensorLocation(location: Point?) { + _faceSensorLocation.tryEmit(location) + } + + fun setFingerprintSensorLocation(location: Point?) { + _fingerprintSensorLocation.tryEmit(location) + } + override fun isUdfpsSupported(): Boolean { return _isUdfpsSupported.value } 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 new file mode 100644 index 0000000000000..7c22604dc5465 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt @@ -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.keyguard.data.repository + +import com.android.systemui.statusbar.LightRevealEffect +import kotlinx.coroutines.flow.MutableStateFlow + +/** Fake implementation of [LightRevealScrimRepository] */ +class FakeLightRevealScrimRepository : LightRevealScrimRepository { + + private val _revealEffect: MutableStateFlow = + MutableStateFlow(DEFAULT_REVEAL_EFFECT) + override val revealEffect = _revealEffect + + fun setRevealEffect(effect: LightRevealEffect) { + _revealEffect.tryEmit(effect) + } +}