diff options
| author | 2024-08-27 03:36:18 +0000 | |
|---|---|---|
| committer | 2024-09-03 23:02:05 +0000 | |
| commit | bed96eb16d84ebd7e771fc4c009141d1305c602f (patch) | |
| tree | c0edacdebc18e0a4d0801c748d097a72e6127ac6 | |
| parent | 5f1c1da0593beea1ab1c8cad42832eac5431fde4 (diff) | |
[flexiglass] Implement fp success and error haptics
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
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<FalsingCollector>().also { kosmos.falsingCollector = it } + private val vibratorHelper = mock<VibratorHelper>().also { kosmos.vibratorHelper = it } private val fakeSceneDataSource = kosmos.fakeSceneDataSource private val windowController = kosmos.notificationShadeWindowController private val centralSurfaces = kosmos.centralSurfaces @@ -634,6 +654,194 @@ class SceneContainerStartableTest : SysuiTestCase() { } @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 { val transitionStateFlow = prepareState() @@ -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, ) } |