summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Grace Cheng <graciecheng@google.com> 2024-08-27 03:36:18 +0000
committer Grace Cheng <graciecheng@google.com> 2024-09-03 23:02:05 +0000
commitbed96eb16d84ebd7e771fc4c009141d1305c602f (patch)
treec0edacdebc18e0a4d0801c748d097a72e6127ac6
parent5f1c1da0593beea1ab1c8cad42832eac5431fde4 (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
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt296
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt4
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,
)
}