Loading packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt +4 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,10 @@ data class BiometricModalities( val hasSfps: Boolean get() = hasFingerprint && fingerprintProperties!!.isAnySidefpsType /** If UDFPS authentication is available. */ val hasUdfps: Boolean get() = hasFingerprint && fingerprintProperties!!.isAnyUdfpsType /** If fingerprint authentication is available (and [faceProperties] is non-null). */ val hasFace: Boolean get() = faceProperties != null Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +16 −0 Original line number Diff line number Diff line Loading @@ -376,6 +376,22 @@ object BiometricViewBinder { } } // Talkback directional guidance backgroundView.setOnHoverListener { _, event -> launch { viewModel.onAnnounceAccessibilityHint( event, accessibilityManager.isTouchExplorationEnabled ) } false } launch { viewModel.accessibilityHint.collect { message -> if (message.isNotBlank()) view.announceForAccessibility(message) } } // Play haptics launch { viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +47 −1 Original line number Diff line number Diff line Loading @@ -21,9 +21,12 @@ import android.hardware.biometrics.BiometricPrompt import android.util.Log import android.view.HapticFeedbackConstants import android.view.MotionEvent import com.android.systemui.Flags.bpTalkback import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.DisplayRotation Loading @@ -35,7 +38,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged Loading @@ -49,7 +54,9 @@ class PromptViewModel constructor( displayStateInteractor: DisplayStateInteractor, promptSelectorInteractor: PromptSelectorInteractor, @Application context: Context, @Application private val context: Context, private val udfpsOverlayInteractor: UdfpsOverlayInteractor, private val udfpsUtils: UdfpsUtils ) { /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = Loading @@ -69,6 +76,11 @@ constructor( val faceIconHeight: Int = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) private val _accessibilityHint = MutableSharedFlow<String>() /** Hint for talkback directional guidance */ val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ Loading Loading @@ -516,6 +528,40 @@ constructor( return false } /** Sets the message used for UDFPS directional guidance */ suspend fun onAnnounceAccessibilityHint( event: MotionEvent, touchExplorationEnabled: Boolean, ): Boolean { if (bpTalkback() && modalities.first().hasUdfps && touchExplorationEnabled) { // TODO(b/315184924): Remove uses of UdfpsUtils val scaledTouch = udfpsUtils.getTouchInNativeCoordinates( event.getPointerId(0), event, udfpsOverlayInteractor.udfpsOverlayParams.value ) if ( !udfpsUtils.isWithinSensorArea( event.getPointerId(0), event, udfpsOverlayInteractor.udfpsOverlayParams.value ) ) { _accessibilityHint.emit( udfpsUtils.onTouchOutsideOfSensorArea( touchExplorationEnabled, context, scaledTouch.x, scaledTouch.y, udfpsOverlayInteractor.udfpsOverlayParams.value ) ) } } return false } /** * Switch to the credential view. * Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +17 −0 Original line number Diff line number Diff line Loading @@ -47,12 +47,14 @@ import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorI import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.events.ANIMATING_OUT import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat Loading Loading @@ -101,6 +103,12 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var vibrator: VibratorHelper @Mock lateinit var udfpsUtils: UdfpsUtils @Mock lateinit var authController: AuthController @Mock lateinit var selectedUserInteractor: SelectedUserInteractor private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) Loading @@ -123,6 +131,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) Loading @@ -140,6 +149,12 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateRepository, displayRepository, ) udfpsOverlayInteractor = UdfpsOverlayInteractor( authController, selectedUserInteractor, testScope.backgroundScope, ) } @After Loading Loading @@ -532,6 +547,8 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateInteractor, promptSelectorInteractor, context, udfpsOverlayInteractor, udfpsUtils ), { credentialViewModel }, Handler(TestableLooper.get(this).looper), Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt +41 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.shared.model import android.hardware.fingerprint.FingerprintSensorProperties import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.faceSensorPropertiesInternal Loading @@ -34,6 +35,46 @@ class BiometricModalitiesTest : SysuiTestCase() { assertThat(BiometricModalities().isEmpty).isTrue() } @Test fun hasUdfps() { with( BiometricModalities( fingerprintProperties = fingerprintSensorPropertiesInternal( sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL ).first(), ) ) { assertThat(isEmpty).isFalse() assertThat(hasUdfps).isTrue() assertThat(hasSfps).isFalse() assertThat(hasFace).isFalse() assertThat(hasFaceOnly).isFalse() assertThat(hasFingerprint).isTrue() assertThat(hasFingerprintOnly).isTrue() assertThat(hasFaceAndFingerprint).isFalse() } } @Test fun hasSfps() { with( BiometricModalities( fingerprintProperties = fingerprintSensorPropertiesInternal( sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON ).first(), ) ) { assertThat(isEmpty).isFalse() assertThat(hasUdfps).isFalse() assertThat(hasSfps).isTrue() assertThat(hasFace).isFalse() assertThat(hasFaceOnly).isFalse() assertThat(hasFingerprint).isTrue() assertThat(hasFingerprintOnly).isTrue() assertThat(hasFaceAndFingerprint).isFalse() } } @Test fun fingerprintOnly() { with( Loading Loading
packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt +4 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,10 @@ data class BiometricModalities( val hasSfps: Boolean get() = hasFingerprint && fingerprintProperties!!.isAnySidefpsType /** If UDFPS authentication is available. */ val hasUdfps: Boolean get() = hasFingerprint && fingerprintProperties!!.isAnyUdfpsType /** If fingerprint authentication is available (and [faceProperties] is non-null). */ val hasFace: Boolean get() = faceProperties != null Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +16 −0 Original line number Diff line number Diff line Loading @@ -376,6 +376,22 @@ object BiometricViewBinder { } } // Talkback directional guidance backgroundView.setOnHoverListener { _, event -> launch { viewModel.onAnnounceAccessibilityHint( event, accessibilityManager.isTouchExplorationEnabled ) } false } launch { viewModel.accessibilityHint.collect { message -> if (message.isNotBlank()) view.announceForAccessibility(message) } } // Play haptics launch { viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +47 −1 Original line number Diff line number Diff line Loading @@ -21,9 +21,12 @@ import android.hardware.biometrics.BiometricPrompt import android.util.Log import android.view.HapticFeedbackConstants import android.view.MotionEvent import com.android.systemui.Flags.bpTalkback import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.DisplayRotation Loading @@ -35,7 +38,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged Loading @@ -49,7 +54,9 @@ class PromptViewModel constructor( displayStateInteractor: DisplayStateInteractor, promptSelectorInteractor: PromptSelectorInteractor, @Application context: Context, @Application private val context: Context, private val udfpsOverlayInteractor: UdfpsOverlayInteractor, private val udfpsUtils: UdfpsUtils ) { /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = Loading @@ -69,6 +76,11 @@ constructor( val faceIconHeight: Int = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) private val _accessibilityHint = MutableSharedFlow<String>() /** Hint for talkback directional guidance */ val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ Loading Loading @@ -516,6 +528,40 @@ constructor( return false } /** Sets the message used for UDFPS directional guidance */ suspend fun onAnnounceAccessibilityHint( event: MotionEvent, touchExplorationEnabled: Boolean, ): Boolean { if (bpTalkback() && modalities.first().hasUdfps && touchExplorationEnabled) { // TODO(b/315184924): Remove uses of UdfpsUtils val scaledTouch = udfpsUtils.getTouchInNativeCoordinates( event.getPointerId(0), event, udfpsOverlayInteractor.udfpsOverlayParams.value ) if ( !udfpsUtils.isWithinSensorArea( event.getPointerId(0), event, udfpsOverlayInteractor.udfpsOverlayParams.value ) ) { _accessibilityHint.emit( udfpsUtils.onTouchOutsideOfSensorArea( touchExplorationEnabled, context, scaledTouch.x, scaledTouch.y, udfpsOverlayInteractor.udfpsOverlayParams.value ) ) } } return false } /** * Switch to the credential view. * Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +17 −0 Original line number Diff line number Diff line Loading @@ -47,12 +47,14 @@ import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorI import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.events.ANIMATING_OUT import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat Loading Loading @@ -101,6 +103,12 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var vibrator: VibratorHelper @Mock lateinit var udfpsUtils: UdfpsUtils @Mock lateinit var authController: AuthController @Mock lateinit var selectedUserInteractor: SelectedUserInteractor private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) Loading @@ -123,6 +131,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) Loading @@ -140,6 +149,12 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateRepository, displayRepository, ) udfpsOverlayInteractor = UdfpsOverlayInteractor( authController, selectedUserInteractor, testScope.backgroundScope, ) } @After Loading Loading @@ -532,6 +547,8 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateInteractor, promptSelectorInteractor, context, udfpsOverlayInteractor, udfpsUtils ), { credentialViewModel }, Handler(TestableLooper.get(this).looper), Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt +41 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.shared.model import android.hardware.fingerprint.FingerprintSensorProperties import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.faceSensorPropertiesInternal Loading @@ -34,6 +35,46 @@ class BiometricModalitiesTest : SysuiTestCase() { assertThat(BiometricModalities().isEmpty).isTrue() } @Test fun hasUdfps() { with( BiometricModalities( fingerprintProperties = fingerprintSensorPropertiesInternal( sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL ).first(), ) ) { assertThat(isEmpty).isFalse() assertThat(hasUdfps).isTrue() assertThat(hasSfps).isFalse() assertThat(hasFace).isFalse() assertThat(hasFaceOnly).isFalse() assertThat(hasFingerprint).isTrue() assertThat(hasFingerprintOnly).isTrue() assertThat(hasFaceAndFingerprint).isFalse() } } @Test fun hasSfps() { with( BiometricModalities( fingerprintProperties = fingerprintSensorPropertiesInternal( sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON ).first(), ) ) { assertThat(isEmpty).isFalse() assertThat(hasUdfps).isFalse() assertThat(hasSfps).isTrue() assertThat(hasFace).isFalse() assertThat(hasFaceOnly).isFalse() assertThat(hasFingerprint).isTrue() assertThat(hasFingerprintOnly).isTrue() assertThat(hasFaceAndFingerprint).isFalse() } } @Test fun fingerprintOnly() { with( Loading