From bed96eb16d84ebd7e771fc4c009141d1305c602f Mon Sep 17 00:00:00 2001 From: Grace Cheng Date: Tue, 27 Aug 2024 03:36:18 +0000 Subject: [flexiglass] Implement fp success and error haptics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements fingerprint success and error haptics across all biometric auth-related scenes: lockscreen, bouncer, quick settings, and shade, and adds tests for the new haptics implementation. Haptics was previously handled in KeyguardRootViewBinder, which was bound in KeyguardViewConfigurator and played the haptics with vibratorHelper.performHapticFeedback(view = KeyguardRootView, feedbackConstant = HapticFeedbackConstants.BIOMETRIC_CONFIRM / HapticFeedbackConstants.BIOMETRIC_REJECT). The new flexiglass implementation doesn’t use KeyguardViewConfigurator, and since haptics is applicable across multiple scenes, we migrate it to SceneContainerStartable, which runs coroutine jobs that outlive the scene transitions. Since it no longer has a view reference, we use vibratorHelper.vibrateAuthSuccess and vibrationHelper.vibrateAuthError to play the haptics. vibrateAuthSuccess uses BIOMETRIC_SUCCESS_VIBRATION_EFFECT, which maps to VibrationEffect.EFFECT_CLICK, the same effect HapticFeedbackVibrationProvider uses for HapticFeedbackConstants.BIOMETRIC_CONFIRM. vibrateAuthError uses BIOMETRIC_ERROR_VIBRATION_EFFECT, which maps to VibrationEffect.EFFECT_DOUBLE_CLICK, the same effect HapticFeedbackVibrationProvider uses for HapticFeedbackConstants.BIOMETRIC_REJECT. Also updates VibrationHelperKosmos, which was using a FakeVibratorHelper type in previous use cases. We need to mock VibrationHelper in test to ensure the expected haptics methods are called, so we create a new Kosmos.fakeVibratorHelper that is the default value for Kosmos.vibratorHelper for previous use cases, and override it to a VibratorHelper mock in this test. Flag: com.android.systemui.scene_container Fixes: 352764632 Fixes: 352766437 Fixes: 352765379 Fixes: 352762251 Test: manually verified success and error haptics on SFPS, UDFPS, RFPS devices on lock screen, primary bouncer, alternate bouncer Test: atest SceneContainerStartableTest Test: atest QSLongPressEffectTest Test: atest SliderHapticFeedbackProviderTest Change-Id: Ic04ae3db137f6413515cb72da22a8d11db6a9a7e --- .../systemui/haptics/qs/QSLongPressEffectTest.kt | 4 +- .../startable/SceneContainerStartableTest.kt | 296 +++++++++++++++++++++ .../domain/startable/SceneContainerStartable.kt | 41 +++ .../slider/SliderHapticFeedbackProviderTest.kt | 11 +- .../systemui/haptics/VibratorHelperKosmos.kt | 4 +- .../startable/SceneContainerStartableKosmos.kt | 4 + 6 files changed, 352 insertions(+), 8 deletions(-) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index fd4ed3896c43..686b518b56e0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -23,7 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator -import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.haptics.fakeVibratorHelper import com.android.systemui.kosmos.testScope import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.qs.qsTileFactory @@ -50,7 +50,7 @@ class QSLongPressEffectTest : SysuiTestCase() { @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() - private val vibratorHelper = kosmos.vibratorHelper + private val vibratorHelper = kosmos.fakeVibratorHelper private val qsTile = kosmos.qsTileFactory.createTile("Test Tile") @Mock private lateinit var callback: QSLongPressEffect.Callback @Mock private lateinit var controller: ActivityTransitionAnimator.Controller diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index d3b51d1d17f7..7bd9690d7983 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.scene.domain.startable import android.app.StatusBarManager +import android.hardware.face.FaceManager import android.os.PowerManager import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -30,6 +31,9 @@ import com.android.internal.policy.IKeyguardDismissCallback import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.shared.logging.BouncerUiEvent @@ -39,8 +43,14 @@ import com.android.systemui.classifier.falsingManager import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository @@ -53,11 +63,15 @@ import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscreenSceneTransitionInteractor +import com.android.systemui.keyguard.shared.model.BiometricUnlockMode +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.model.sysUiState import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.data.repository.powerRepository import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor @@ -69,6 +83,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shared.system.QuickStepContract +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository @@ -85,6 +100,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -92,6 +108,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.times @@ -105,12 +123,14 @@ class SceneContainerStartableTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val deviceEntryHapticsInteractor by lazy { kosmos.deviceEntryHapticsInteractor } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository } private val bouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } private val sysUiState = kosmos.sysUiState private val falsingCollector = mock().also { kosmos.falsingCollector = it } + private val vibratorHelper = mock().also { kosmos.vibratorHelper = it } private val fakeSceneDataSource = kosmos.fakeSceneDataSource private val windowController = kosmos.notificationShadeWindowController private val centralSurfaces = kosmos.centralSurfaces @@ -633,6 +653,194 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } + @Test + fun playSuccessHaptics_onSuccessfulLockscreenAuth_udfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasUdfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper) + .vibrateAuthSuccess( + "SceneContainerStartable, $currentSceneKey device-entry::success" + ) + verify(vibratorHelper, never()).vibrateAuthError(anyString()) + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + fun playSuccessHaptics_onSuccessfulLockscreenAuth_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + allowHapticsOnSfps() + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper) + .vibrateAuthSuccess( + "SceneContainerStartable, $currentSceneKey device-entry::success" + ) + verify(vibratorHelper, never()).vibrateAuthError(anyString()) + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + fun playErrorHaptics_onFailedLockscreenAuth_udfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasUdfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + updateFingerprintAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper) + .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) + } + + @Test + fun playErrorHaptics_onFailedLockscreenAuth_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + updateFingerprintAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper) + .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) + } + + @Test + fun skipsSuccessHaptics_whenPowerButtonDown_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + allowHapticsOnSfps(isPowerButtonDown = true) + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper, never()) + .vibrateAuthSuccess( + "SceneContainerStartable, $currentSceneKey device-entry::success" + ) + verify(vibratorHelper, never()).vibrateAuthError(anyString()) + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + fun skipsSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + allowHapticsOnSfps(lastPowerPress = 50) + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper, never()) + .vibrateAuthSuccess( + "SceneContainerStartable, $currentSceneKey device-entry::success" + ) + verify(vibratorHelper, never()).vibrateAuthError(anyString()) + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + fun skipsErrorHaptics_whenPowerButtonDown_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + kosmos.fakeKeyEventRepository.setPowerButtonDown(true) + updateFingerprintAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper, never()) + .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) + } + + @Test + fun skipsFaceErrorHaptics_nonSfps_coEx() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasUdfps = true, hasFace = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + updateFaceAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + verify(vibratorHelper, never()) + .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) + } + @Test fun hydrateSystemUiState() = testScope.runTest { @@ -1876,4 +2084,92 @@ class SceneContainerStartableTest : SysuiTestCase() { FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { this.isPinned.value = isPinned } + + private fun setFingerprintSensorType(fingerprintSensorType: FingerprintSensorType) { + kosmos.fingerprintPropertyRepository.setProperties( + sensorId = 0, + strength = SensorStrength.STRONG, + sensorType = fingerprintSensorType, + sensorLocations = mapOf(), + ) + kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + } + + private fun setFaceEnrolled() { + kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + } + + private fun TestScope.allowHapticsOnSfps( + isPowerButtonDown: Boolean = false, + lastPowerPress: Long = 10000 + ) { + kosmos.fakeKeyEventRepository.setPowerButtonDown(isPowerButtonDown) + + kosmos.powerRepository.updateWakefulness( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + powerButtonLaunchGestureTriggered = false, + ) + + advanceTimeBy(lastPowerPress) + runCurrent() + } + + private fun unlockWithFingerprintAuth() { + kosmos.fakeKeyguardRepository.setBiometricUnlockSource( + BiometricUnlockSource.FINGERPRINT_SENSOR + ) + kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.UNLOCK_COLLAPSING) + } + + private fun TestScope.setupBiometricAuth( + hasSfps: Boolean = false, + hasUdfps: Boolean = false, + hasFace: Boolean = false + ) { + if (hasSfps) { + setFingerprintSensorType(FingerprintSensorType.POWER_BUTTON) + } + + if (hasUdfps) { + setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + } + + if (hasFace) { + setFaceEnrolled() + } + + prepareState( + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + initialSceneKey = Scenes.Lockscreen, + ) + } + + private fun updateFingerprintAuthStatus(isSuccess: Boolean) { + if (isSuccess) { + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + } else { + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + FailFingerprintAuthenticationStatus + ) + } + } + + private fun updateFaceAuthStatus(isSuccess: Boolean) { + if (isSuccess) { + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + SuccessFaceAuthenticationStatus( + successResult = Mockito.mock(FaceManager.AuthenticationResult::class.java) + ) + ) + } else { + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + FailedFaceAuthenticationStatus() + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 0a7526a41d65..58dcc6f37307 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -36,6 +36,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.DisplayId import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource @@ -62,6 +63,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor @@ -78,6 +80,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -107,6 +110,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val sceneInteractor: SceneInteractor, private val deviceEntryInteractor: DeviceEntryInteractor, + private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val bouncerInteractor: BouncerInteractor, private val keyguardInteractor: KeyguardInteractor, @@ -134,6 +138,7 @@ constructor( private val dismissCallbackRegistry: DismissCallbackRegistry, private val statusBarStateController: SysuiStatusBarStateController, private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val vibratorHelper: VibratorHelper, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -148,6 +153,7 @@ constructor( respondToFalsingDetections() hydrateInteractionState() handleBouncerOverscroll() + handleDeviceEntryHapticsWhileDeviceLocked() hydrateWindowController() hydrateBackStack() resetShadeSessions() @@ -525,6 +531,37 @@ constructor( } } + private fun handleDeviceEntryHapticsWhileDeviceLocked() { + applicationScope.launch { + deviceEntryInteractor.isDeviceEntered.collectLatest { isDeviceEntered -> + // Only check for haptics signals before device is entered + if (!isDeviceEntered) { + coroutineScope { + launch { + deviceEntryHapticsInteractor.playSuccessHaptic + .sample(sceneInteractor.currentScene) + .collect { currentScene -> + vibratorHelper.vibrateAuthSuccess( + "$TAG, $currentScene device-entry::success" + ) + } + } + + launch { + deviceEntryHapticsInteractor.playErrorHaptic + .sample(sceneInteractor.currentScene) + .collect { currentScene -> + vibratorHelper.vibrateAuthError( + "$TAG, $currentScene device-entry::error" + ) + } + } + } + } + } + } + } + /** Keeps [SysUiState] up-to-date */ private fun hydrateSystemUiState() { applicationScope.launch { @@ -817,4 +854,8 @@ constructor( .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() } } } + + companion object { + private const val TAG = "SceneContainerStartable" + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index 933ddb5739e9..4a80d7242e03 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt @@ -21,7 +21,7 @@ import android.view.VelocityTracker import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.haptics.fakeVibratorHelper import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.fakeSystemClock @@ -47,6 +47,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { private val lowTickDuration = 12 // Mocked duration of a low tick private val dragTextureThresholdMillis = lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval + private val vibratorHelper = kosmos.fakeVibratorHelper private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider @Before @@ -56,11 +57,11 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { whenever(velocityTracker.getAxisVelocity(config.velocityAxis)) .thenReturn(config.maxVelocityToScale) - kosmos.vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] = + vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] = lowTickDuration sliderHapticFeedbackProvider = SliderHapticFeedbackProvider( - kosmos.vibratorHelper, + vibratorHelper, velocityTracker, config, kosmos.fakeSystemClock, @@ -136,7 +137,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onUpperBookend() sliderHapticFeedbackProvider.onUpperBookend() - assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(vibration)) + assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(vibration)) } @Test @@ -162,7 +163,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { sliderHapticFeedbackProvider.onProgress(progress) // THEN the correct composition only plays once - assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(ticks.compose())) + assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(ticks.compose())) } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt index 434953fb2f43..ff71f2f391e2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt @@ -17,5 +17,7 @@ package com.android.systemui.haptics import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.VibratorHelper -var Kosmos.vibratorHelper by Kosmos.Fixture { FakeVibratorHelper() } +var Kosmos.vibratorHelper: VibratorHelper by Kosmos.Fixture { fakeVibratorHelper } +val Kosmos.fakeVibratorHelper by Kosmos.Fixture { FakeVibratorHelper() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 53b6a2ee226b..b612a8b5893a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -24,8 +24,10 @@ import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.falsingCollector import com.android.systemui.classifier.falsingManager import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -55,6 +57,7 @@ val Kosmos.sceneContainerStartable by Fixture { applicationScope = testScope.backgroundScope, sceneInteractor = sceneInteractor, deviceEntryInteractor = deviceEntryInteractor, + deviceEntryHapticsInteractor = deviceEntryHapticsInteractor, deviceUnlockedInteractor = deviceUnlockedInteractor, bouncerInteractor = bouncerInteractor, keyguardInteractor = keyguardInteractor, @@ -82,5 +85,6 @@ val Kosmos.sceneContainerStartable by Fixture { dismissCallbackRegistry = dismissCallbackRegistry, statusBarStateController = sysuiStatusBarStateController, alternateBouncerInteractor = alternateBouncerInteractor, + vibratorHelper = vibratorHelper, ) } -- cgit v1.2.3-59-g8ed1b