Loading packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +3 −2 Original line number Original line Diff line number Diff line Loading @@ -646,8 +646,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { shouldPilfer = true; shouldPilfer = true; } } // Pilfer only once per gesture // Pilfer only once per gesture, don't pilfer for BP if (shouldPilfer && !mPointerPilfered) { if (shouldPilfer && !mPointerPilfered && getBiometricSessionType() != SESSION_BIOMETRIC_PROMPT) { mInputManager.pilferPointers( mInputManager.pilferPointers( mOverlay.getOverlayView().getViewRootImpl().getInputToken()); mOverlay.getOverlayView().getViewRootImpl().getInputToken()); mPointerPilfered = true; mPointerPilfered = true; Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +18 −9 Original line number Original line Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.binder package com.android.systemui.biometrics.ui.binder import android.animation.Animator import android.animation.Animator import android.annotation.SuppressLint import android.content.Context import android.content.Context import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricConstants Loading @@ -26,6 +27,7 @@ import android.os.Bundle import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod import android.util.Log import android.util.Log import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager Loading Loading @@ -73,6 +75,7 @@ private const val TAG = "BiometricViewBinder" object BiometricViewBinder { object BiometricViewBinder { /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ @SuppressLint("ClickableViewAccessibility") @JvmStatic @JvmStatic fun bind( fun bind( view: BiometricPromptLayout, view: BiometricPromptLayout, Loading Loading @@ -300,21 +303,19 @@ object BiometricViewBinder { // reuse the icon as a confirm button // reuse the icon as a confirm button launch { launch { viewModel.isConfirmButtonVisible viewModel.isIconConfirmButton .map { isPending -> .map { isPending -> when { when { isPending && iconController.actsAsConfirmButton -> isPending && iconController.actsAsConfirmButton -> View.OnClickListener { viewModel.confirmAuthenticated() } View.OnTouchListener { _: View, event: MotionEvent -> else -> null viewModel.onOverlayTouch(event) } } else -> null } } .collect { onClick -> iconViewOverlay.setOnClickListener(onClick) iconView.setOnClickListener(onClick) if (onClick == null) { iconViewOverlay.isClickable = false iconView.isClickable = false } } .collect { onTouch -> iconViewOverlay.setOnTouchListener(onTouch) iconView.setOnTouchListener(onTouch) } } } } Loading @@ -340,6 +341,14 @@ object BiometricViewBinder { backgroundView.setOnClickListener(null) backgroundView.setOnClickListener(null) backgroundView.importantForAccessibility = backgroundView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO IMPORTANT_FOR_ACCESSIBILITY_NO // Allow icon to be used as confirmation button with a11y enabled if (accessibilityManager.isTouchExplorationEnabled) { iconViewOverlay.setOnClickListener { viewModel.confirmAuthenticated() } iconView.setOnClickListener { viewModel.confirmAuthenticated() } } } } if (authState.isAuthenticatedAndConfirmed) { if (authState.isAuthenticatedAndConfirmed) { view.announceForAccessibility( view.announceForAccessibility( Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +40 −12 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.hardware.biometrics.BiometricPrompt 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 com.android.systemui.biometrics.AuthBiometricView import com.android.systemui.biometrics.AuthBiometricView import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.domain.model.BiometricModalities Loading Loading @@ -67,11 +68,18 @@ constructor( /** If the user has successfully authenticated and confirmed (when explicitly required). */ /** If the user has successfully authenticated and confirmed (when explicitly required). */ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) /** /** * If the API caller or the user's personal preferences require explicit confirmation after * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. * successful authentication. */ */ val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired val isConfirmationRequired: Flow<Boolean> = combine(_isOverlayTouched, interactor.isConfirmationRequired) { isOverlayTouched, isConfirmationRequired -> !isOverlayTouched && isConfirmationRequired } /** The kind of credential the user has. */ /** The kind of credential the user has. */ val credentialKind: Flow<PromptKind> = interactor.credentialKind val credentialKind: Flow<PromptKind> = interactor.credentialKind Loading Loading @@ -150,6 +158,12 @@ constructor( } } .distinctUntilChanged() .distinctUntilChanged() /** If the icon can be used as a confirmation button. */ val isIconConfirmButton: Flow<Boolean> = combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired -> size.isNotSmall && isConfirmationRequired } /** If the negative button should be shown. */ /** If the negative button should be shown. */ val isNegativeButtonVisible: Flow<Boolean> = val isNegativeButtonVisible: Flow<Boolean> = combine( combine( Loading Loading @@ -298,8 +312,10 @@ constructor( if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true _forceMediumSize.value = true _legacyState.value = _legacyState.value = if (alreadyAuthenticated) { if (alreadyAuthenticated && isConfirmationRequired.first()) { AuthBiometricView.STATE_PENDING_CONFIRMATION AuthBiometricView.STATE_PENDING_CONFIRMATION } else if (alreadyAuthenticated && !isConfirmationRequired.first()) { AuthBiometricView.STATE_AUTHENTICATED } else { } else { AuthBiometricView.STATE_HELP AuthBiometricView.STATE_HELP } } Loading Loading @@ -397,18 +413,10 @@ constructor( } } private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { val availableModalities = modalities.first() val confirmationRequired = isConfirmationRequired.first() val confirmationRequired = isConfirmationRequired.first() if (availableModalities.hasFaceAndFingerprint) { // Only worry about confirmationRequired if face was used to unlock // coex only needs confirmation when face is successful, unless it happens on the // first attempt (i.e. without failure) before fingerprint scanning starts val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending if (modality == BiometricModality.Face) { if (modality == BiometricModality.Face) { return fingerprintStarted || confirmationRequired } } if (availableModalities.hasFaceOnly) { return confirmationRequired return confirmationRequired } } // fingerprint only never requires confirmation // fingerprint only never requires confirmation Loading Loading @@ -438,6 +446,26 @@ constructor( messageJob = null messageJob = null } } /** * Touch event occurred on the overlay * * Tracks whether a finger is currently down to set [_isOverlayTouched] to be used as user * confirmation */ fun onOverlayTouch(event: MotionEvent): Boolean { if (event.actionMasked == MotionEvent.ACTION_DOWN) { _isOverlayTouched.value = true if (_isAuthenticated.value.needsUserConfirmation) { confirmAuthenticated() } return true } else if (event.actionMasked == MotionEvent.ACTION_UP) { _isOverlayTouched.value = false } return false } /** /** * Switch to the credential view. * Switch to the credential view. * * Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +6 −1 Original line number Original line Diff line number Diff line Loading @@ -527,6 +527,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) val legacyState by collectLastValue(viewModel.legacyState) val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired) if (testCase.isCoex && testCase.authenticatedByFingerprint) { if (testCase.isCoex && testCase.authenticatedByFingerprint) { viewModel.ensureFingerprintHasStarted(isDelayed = true) viewModel.ensureFingerprintHasStarted(isDelayed = true) Loading @@ -535,7 +536,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.showHelp(helpMessage) viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) assertThat(size).isEqualTo(PromptSize.MEDIUM) if (confirmationRequired == true) { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) } else { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) } assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() assertThat(messageVisible).isTrue() assertThat(authenticating).isFalse() assertThat(authenticating).isFalse() Loading Loading
packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +3 −2 Original line number Original line Diff line number Diff line Loading @@ -646,8 +646,9 @@ public class UdfpsController implements DozeReceiver, Dumpable { shouldPilfer = true; shouldPilfer = true; } } // Pilfer only once per gesture // Pilfer only once per gesture, don't pilfer for BP if (shouldPilfer && !mPointerPilfered) { if (shouldPilfer && !mPointerPilfered && getBiometricSessionType() != SESSION_BIOMETRIC_PROMPT) { mInputManager.pilferPointers( mInputManager.pilferPointers( mOverlay.getOverlayView().getViewRootImpl().getInputToken()); mOverlay.getOverlayView().getViewRootImpl().getInputToken()); mPointerPilfered = true; mPointerPilfered = true; Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +18 −9 Original line number Original line Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.binder package com.android.systemui.biometrics.ui.binder import android.animation.Animator import android.animation.Animator import android.annotation.SuppressLint import android.content.Context import android.content.Context import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricConstants Loading @@ -26,6 +27,7 @@ import android.os.Bundle import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod import android.util.Log import android.util.Log import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager Loading Loading @@ -73,6 +75,7 @@ private const val TAG = "BiometricViewBinder" object BiometricViewBinder { object BiometricViewBinder { /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ @SuppressLint("ClickableViewAccessibility") @JvmStatic @JvmStatic fun bind( fun bind( view: BiometricPromptLayout, view: BiometricPromptLayout, Loading Loading @@ -300,21 +303,19 @@ object BiometricViewBinder { // reuse the icon as a confirm button // reuse the icon as a confirm button launch { launch { viewModel.isConfirmButtonVisible viewModel.isIconConfirmButton .map { isPending -> .map { isPending -> when { when { isPending && iconController.actsAsConfirmButton -> isPending && iconController.actsAsConfirmButton -> View.OnClickListener { viewModel.confirmAuthenticated() } View.OnTouchListener { _: View, event: MotionEvent -> else -> null viewModel.onOverlayTouch(event) } } else -> null } } .collect { onClick -> iconViewOverlay.setOnClickListener(onClick) iconView.setOnClickListener(onClick) if (onClick == null) { iconViewOverlay.isClickable = false iconView.isClickable = false } } .collect { onTouch -> iconViewOverlay.setOnTouchListener(onTouch) iconView.setOnTouchListener(onTouch) } } } } Loading @@ -340,6 +341,14 @@ object BiometricViewBinder { backgroundView.setOnClickListener(null) backgroundView.setOnClickListener(null) backgroundView.importantForAccessibility = backgroundView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO IMPORTANT_FOR_ACCESSIBILITY_NO // Allow icon to be used as confirmation button with a11y enabled if (accessibilityManager.isTouchExplorationEnabled) { iconViewOverlay.setOnClickListener { viewModel.confirmAuthenticated() } iconView.setOnClickListener { viewModel.confirmAuthenticated() } } } } if (authState.isAuthenticatedAndConfirmed) { if (authState.isAuthenticatedAndConfirmed) { view.announceForAccessibility( view.announceForAccessibility( Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +40 −12 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.hardware.biometrics.BiometricPrompt 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 com.android.systemui.biometrics.AuthBiometricView import com.android.systemui.biometrics.AuthBiometricView import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.domain.model.BiometricModalities Loading Loading @@ -67,11 +68,18 @@ constructor( /** If the user has successfully authenticated and confirmed (when explicitly required). */ /** If the user has successfully authenticated and confirmed (when explicitly required). */ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) /** /** * If the API caller or the user's personal preferences require explicit confirmation after * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. * successful authentication. */ */ val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired val isConfirmationRequired: Flow<Boolean> = combine(_isOverlayTouched, interactor.isConfirmationRequired) { isOverlayTouched, isConfirmationRequired -> !isOverlayTouched && isConfirmationRequired } /** The kind of credential the user has. */ /** The kind of credential the user has. */ val credentialKind: Flow<PromptKind> = interactor.credentialKind val credentialKind: Flow<PromptKind> = interactor.credentialKind Loading Loading @@ -150,6 +158,12 @@ constructor( } } .distinctUntilChanged() .distinctUntilChanged() /** If the icon can be used as a confirmation button. */ val isIconConfirmButton: Flow<Boolean> = combine(size, interactor.isConfirmationRequired) { size, isConfirmationRequired -> size.isNotSmall && isConfirmationRequired } /** If the negative button should be shown. */ /** If the negative button should be shown. */ val isNegativeButtonVisible: Flow<Boolean> = val isNegativeButtonVisible: Flow<Boolean> = combine( combine( Loading Loading @@ -298,8 +312,10 @@ constructor( if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true _forceMediumSize.value = true _legacyState.value = _legacyState.value = if (alreadyAuthenticated) { if (alreadyAuthenticated && isConfirmationRequired.first()) { AuthBiometricView.STATE_PENDING_CONFIRMATION AuthBiometricView.STATE_PENDING_CONFIRMATION } else if (alreadyAuthenticated && !isConfirmationRequired.first()) { AuthBiometricView.STATE_AUTHENTICATED } else { } else { AuthBiometricView.STATE_HELP AuthBiometricView.STATE_HELP } } Loading Loading @@ -397,18 +413,10 @@ constructor( } } private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { val availableModalities = modalities.first() val confirmationRequired = isConfirmationRequired.first() val confirmationRequired = isConfirmationRequired.first() if (availableModalities.hasFaceAndFingerprint) { // Only worry about confirmationRequired if face was used to unlock // coex only needs confirmation when face is successful, unless it happens on the // first attempt (i.e. without failure) before fingerprint scanning starts val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending if (modality == BiometricModality.Face) { if (modality == BiometricModality.Face) { return fingerprintStarted || confirmationRequired } } if (availableModalities.hasFaceOnly) { return confirmationRequired return confirmationRequired } } // fingerprint only never requires confirmation // fingerprint only never requires confirmation Loading Loading @@ -438,6 +446,26 @@ constructor( messageJob = null messageJob = null } } /** * Touch event occurred on the overlay * * Tracks whether a finger is currently down to set [_isOverlayTouched] to be used as user * confirmation */ fun onOverlayTouch(event: MotionEvent): Boolean { if (event.actionMasked == MotionEvent.ACTION_DOWN) { _isOverlayTouched.value = true if (_isAuthenticated.value.needsUserConfirmation) { confirmAuthenticated() } return true } else if (event.actionMasked == MotionEvent.ACTION_UP) { _isOverlayTouched.value = false } return false } /** /** * Switch to the credential view. * Switch to the credential view. * * Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +6 −1 Original line number Original line Diff line number Diff line Loading @@ -527,6 +527,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) val legacyState by collectLastValue(viewModel.legacyState) val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired) if (testCase.isCoex && testCase.authenticatedByFingerprint) { if (testCase.isCoex && testCase.authenticatedByFingerprint) { viewModel.ensureFingerprintHasStarted(isDelayed = true) viewModel.ensureFingerprintHasStarted(isDelayed = true) Loading @@ -535,7 +536,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.showHelp(helpMessage) viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) assertThat(size).isEqualTo(PromptSize.MEDIUM) if (confirmationRequired == true) { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) } else { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) } assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() assertThat(messageVisible).isTrue() assertThat(authenticating).isFalse() assertThat(authenticating).isFalse() Loading