Loading packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt +4 −0 Original line number Original line Diff line number Diff line Loading @@ -37,6 +37,10 @@ data class BiometricModalities( val hasSfps: Boolean val hasSfps: Boolean get() = hasFingerprint && fingerprintProperties!!.isAnySidefpsType 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). */ /** If fingerprint authentication is available (and [faceProperties] is non-null). */ val hasFace: Boolean val hasFace: Boolean get() = faceProperties != null get() = faceProperties != null Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +16 −0 Original line number Original line 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 // Play haptics launch { launch { viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +47 −1 Original line number Original line Diff line number Diff line Loading @@ -21,9 +21,12 @@ import android.hardware.biometrics.BiometricPrompt import android.util.Log import android.util.Log import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants import android.view.MotionEvent 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.Utils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor 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.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.DisplayRotation Loading @@ -35,7 +38,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged Loading @@ -49,7 +54,9 @@ class PromptViewModel constructor( constructor( displayStateInteractor: DisplayStateInteractor, displayStateInteractor: DisplayStateInteractor, promptSelectorInteractor: PromptSelectorInteractor, 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 */ /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = val modalities: Flow<BiometricModalities> = Loading @@ -69,6 +76,11 @@ constructor( val faceIconHeight: Int = val faceIconHeight: Int = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) 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) private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ Loading Loading @@ -516,6 +528,40 @@ constructor( return false 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. * Switch to the credential view. * * Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +17 −0 Original line number Original line 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.FakeCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl 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.CredentialViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.events.ANIMATING_OUT 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.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat Loading Loading @@ -101,6 +103,12 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var interactionJankMonitor: InteractionJankMonitor lateinit var interactionJankMonitor: InteractionJankMonitor @Mock @Mock lateinit var vibrator: VibratorHelper 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 testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) Loading @@ -123,6 +131,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) Loading @@ -140,6 +149,12 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateRepository, displayStateRepository, displayRepository, displayRepository, ) ) udfpsOverlayInteractor = UdfpsOverlayInteractor( authController, selectedUserInteractor, testScope.backgroundScope, ) } } @After @After Loading Loading @@ -532,6 +547,8 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateInteractor, displayStateInteractor, promptSelectorInteractor, promptSelectorInteractor, context, context, udfpsOverlayInteractor, udfpsUtils ), ), { credentialViewModel }, { credentialViewModel }, Handler(TestableLooper.get(this).looper), Handler(TestableLooper.get(this).looper), Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt +41 −0 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.shared.model package com.android.systemui.biometrics.shared.model import android.hardware.fingerprint.FingerprintSensorProperties import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.faceSensorPropertiesInternal import com.android.systemui.biometrics.faceSensorPropertiesInternal Loading @@ -34,6 +35,46 @@ class BiometricModalitiesTest : SysuiTestCase() { assertThat(BiometricModalities().isEmpty).isTrue() 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 @Test fun fingerprintOnly() { fun fingerprintOnly() { with( with( Loading Loading
packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt +4 −0 Original line number Original line Diff line number Diff line Loading @@ -37,6 +37,10 @@ data class BiometricModalities( val hasSfps: Boolean val hasSfps: Boolean get() = hasFingerprint && fingerprintProperties!!.isAnySidefpsType 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). */ /** If fingerprint authentication is available (and [faceProperties] is non-null). */ val hasFace: Boolean val hasFace: Boolean get() = faceProperties != null get() = faceProperties != null Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +16 −0 Original line number Original line 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 // Play haptics launch { launch { viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> viewModel.hapticsToPlay.collect { hapticFeedbackConstant -> Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +47 −1 Original line number Original line Diff line number Diff line Loading @@ -21,9 +21,12 @@ import android.hardware.biometrics.BiometricPrompt import android.util.Log import android.util.Log import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants import android.view.MotionEvent 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.Utils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor 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.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.DisplayRotation Loading @@ -35,7 +38,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged Loading @@ -49,7 +54,9 @@ class PromptViewModel constructor( constructor( displayStateInteractor: DisplayStateInteractor, displayStateInteractor: DisplayStateInteractor, promptSelectorInteractor: PromptSelectorInteractor, 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 */ /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = val modalities: Flow<BiometricModalities> = Loading @@ -69,6 +76,11 @@ constructor( val faceIconHeight: Int = val faceIconHeight: Int = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) 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) private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ Loading Loading @@ -516,6 +528,40 @@ constructor( return false 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. * Switch to the credential view. * * Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +17 −0 Original line number Original line 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.FakeCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl 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.CredentialViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.events.ANIMATING_OUT 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.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat Loading Loading @@ -101,6 +103,12 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var interactionJankMonitor: InteractionJankMonitor lateinit var interactionJankMonitor: InteractionJankMonitor @Mock @Mock lateinit var vibrator: VibratorHelper 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 testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) Loading @@ -123,6 +131,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayRepository: FakeDisplayRepository private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) Loading @@ -140,6 +149,12 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateRepository, displayStateRepository, displayRepository, displayRepository, ) ) udfpsOverlayInteractor = UdfpsOverlayInteractor( authController, selectedUserInteractor, testScope.backgroundScope, ) } } @After @After Loading Loading @@ -532,6 +547,8 @@ open class AuthContainerViewTest : SysuiTestCase() { displayStateInteractor, displayStateInteractor, promptSelectorInteractor, promptSelectorInteractor, context, context, udfpsOverlayInteractor, udfpsUtils ), ), { credentialViewModel }, { credentialViewModel }, Handler(TestableLooper.get(this).looper), Handler(TestableLooper.get(this).looper), Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt +41 −0 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.shared.model package com.android.systemui.biometrics.shared.model import android.hardware.fingerprint.FingerprintSensorProperties import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.faceSensorPropertiesInternal import com.android.systemui.biometrics.faceSensorPropertiesInternal Loading @@ -34,6 +35,46 @@ class BiometricModalitiesTest : SysuiTestCase() { assertThat(BiometricModalities().isEmpty).isTrue() 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 @Test fun fingerprintOnly() { fun fingerprintOnly() { with( with( Loading