Loading packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +1 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" android:importantForAccessibility="yes" android:importantForAccessibility="auto" android:clickable="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/rightGuideline" Loading packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml +1 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,7 @@ android:layout_height="match_parent"> android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" android:importantForAccessibility="yes" android:importantForAccessibility="auto" android:clickable="false" android:paddingHorizontal="16dp" android:paddingVertical="16dp" Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +37 −23 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES import android.view.accessibility.AccessibilityManager import android.widget.Button import android.widget.ImageView Loading @@ -43,7 +44,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.biometrics.Utils.ellipsize import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality Loading @@ -63,6 +63,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch private const val TAG = "BiometricViewBinder" Loading Loading @@ -123,6 +124,19 @@ object BiometricViewBinder { val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) val retryButton = view.requireViewById<Button>(R.id.button_try_again) // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( view = view, viewModel = viewModel, applicationContext = view.context.applicationContext, applicationScope = applicationScope, ) // bind to prompt var boundSize = false view.repeatWhenAttached { // Handles custom "Cancel Authentication" talkback action val cancelDelegate: AccessibilityDelegateCompat = object : AccessibilityDelegateCompat() { Loading @@ -131,10 +145,18 @@ object BiometricViewBinder { info: AccessibilityNodeInfoCompat, ) { super.onInitializeAccessibilityNodeInfo(host, info) lifecycleScope.launch { // Clears UDFPS guidance hint after focus moves to cancel view viewModel.onClearUdfpsGuidanceHint( accessibilityManager.isTouchExplorationEnabled ) } info.addAction( AccessibilityActionCompat( AccessibilityNodeInfoCompat.ACTION_CLICK, view.context.getString(R.string.biometric_dialog_cancel_authentication), view.context.getString( R.string.biometric_dialog_cancel_authentication ), ) ) } Loading @@ -142,19 +164,6 @@ object BiometricViewBinder { ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate) ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate) // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( view = view, viewModel = viewModel, applicationContext = view.context.applicationContext, applicationScope = applicationScope, ) // bind to prompt var boundSize = false view.repeatWhenAttached { // these do not change and need to be set before any size transitions val modalities = viewModel.modalities.first() Loading Loading @@ -404,11 +413,16 @@ object BiometricViewBinder { } false } launch { viewModel.accessibilityHint.collect { message -> if (message.isNotBlank()) { udfpsGuidanceView.contentDescription = message udfpsGuidanceView.importantForAccessibility = if (message == null) { IMPORTANT_FOR_ACCESSIBILITY_NO } else { IMPORTANT_FOR_ACCESSIBILITY_YES } udfpsGuidanceView.contentDescription = message } } Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +15 −2 Original line number Diff line number Diff line Loading @@ -187,10 +187,10 @@ constructor( } } private val _accessibilityHint = MutableSharedFlow<String>() private val _accessibilityHint = MutableSharedFlow<String?>() /** Hint for talkback directional guidance */ val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() val accessibilityHint: Flow<String?> = _accessibilityHint.asSharedFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) Loading Loading @@ -923,6 +923,19 @@ constructor( return false } /** Clears the message used for UDFPS directional guidance */ suspend fun onClearUdfpsGuidanceHint(touchExplorationEnabled: Boolean) { if ( modalities.first().hasUdfps && touchExplorationEnabled && !isAuthenticated.first().isAuthenticated ) { // Add delay to make sure we read the guidance message before clearing it delay(1000) _accessibilityHint.emit(null) } } /** * Switch to the credential view. * Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +8 −0 Original line number Diff line number Diff line Loading @@ -1482,6 +1482,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } else { assertThat(hint.isNullOrBlank()).isTrue() } kosmos.promptViewModel.onClearUdfpsGuidanceHint(true) if (testCase.modalities.hasUdfps) { assertThat(hint).isNull() } else { assertThat(hint.isNullOrBlank()).isTrue() } } @Test Loading Loading
packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml +1 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" android:importantForAccessibility="yes" android:importantForAccessibility="auto" android:clickable="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/rightGuideline" Loading
packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml +1 −1 Original line number Diff line number Diff line Loading @@ -23,7 +23,7 @@ android:layout_height="match_parent"> android:layout_width="0dp" android:layout_height="0dp" android:accessibilityLiveRegion="assertive" android:importantForAccessibility="yes" android:importantForAccessibility="auto" android:clickable="false" android:paddingHorizontal="16dp" android:paddingVertical="16dp" Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +37 −23 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import android.util.Log import android.view.MotionEvent import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES import android.view.accessibility.AccessibilityManager import android.widget.Button import android.widget.ImageView Loading @@ -43,7 +44,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.biometrics.Utils.ellipsize import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality Loading @@ -63,6 +63,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch private const val TAG = "BiometricViewBinder" Loading Loading @@ -123,6 +124,19 @@ object BiometricViewBinder { val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) val retryButton = view.requireViewById<Button>(R.id.button_try_again) // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( view = view, viewModel = viewModel, applicationContext = view.context.applicationContext, applicationScope = applicationScope, ) // bind to prompt var boundSize = false view.repeatWhenAttached { // Handles custom "Cancel Authentication" talkback action val cancelDelegate: AccessibilityDelegateCompat = object : AccessibilityDelegateCompat() { Loading @@ -131,10 +145,18 @@ object BiometricViewBinder { info: AccessibilityNodeInfoCompat, ) { super.onInitializeAccessibilityNodeInfo(host, info) lifecycleScope.launch { // Clears UDFPS guidance hint after focus moves to cancel view viewModel.onClearUdfpsGuidanceHint( accessibilityManager.isTouchExplorationEnabled ) } info.addAction( AccessibilityActionCompat( AccessibilityNodeInfoCompat.ACTION_CLICK, view.context.getString(R.string.biometric_dialog_cancel_authentication), view.context.getString( R.string.biometric_dialog_cancel_authentication ), ) ) } Loading @@ -142,19 +164,6 @@ object BiometricViewBinder { ViewCompat.setAccessibilityDelegate(backgroundView, cancelDelegate) ViewCompat.setAccessibilityDelegate(cancelButton, cancelDelegate) // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( view = view, viewModel = viewModel, applicationContext = view.context.applicationContext, applicationScope = applicationScope, ) // bind to prompt var boundSize = false view.repeatWhenAttached { // these do not change and need to be set before any size transitions val modalities = viewModel.modalities.first() Loading Loading @@ -404,11 +413,16 @@ object BiometricViewBinder { } false } launch { viewModel.accessibilityHint.collect { message -> if (message.isNotBlank()) { udfpsGuidanceView.contentDescription = message udfpsGuidanceView.importantForAccessibility = if (message == null) { IMPORTANT_FOR_ACCESSIBILITY_NO } else { IMPORTANT_FOR_ACCESSIBILITY_YES } udfpsGuidanceView.contentDescription = message } } Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +15 −2 Original line number Diff line number Diff line Loading @@ -187,10 +187,10 @@ constructor( } } private val _accessibilityHint = MutableSharedFlow<String>() private val _accessibilityHint = MutableSharedFlow<String?>() /** Hint for talkback directional guidance */ val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() val accessibilityHint: Flow<String?> = _accessibilityHint.asSharedFlow() private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) Loading Loading @@ -923,6 +923,19 @@ constructor( return false } /** Clears the message used for UDFPS directional guidance */ suspend fun onClearUdfpsGuidanceHint(touchExplorationEnabled: Boolean) { if ( modalities.first().hasUdfps && touchExplorationEnabled && !isAuthenticated.first().isAuthenticated ) { // Add delay to make sure we read the guidance message before clearing it delay(1000) _accessibilityHint.emit(null) } } /** * Switch to the credential view. * Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +8 −0 Original line number Diff line number Diff line Loading @@ -1482,6 +1482,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } else { assertThat(hint.isNullOrBlank()).isTrue() } kosmos.promptViewModel.onClearUdfpsGuidanceHint(true) if (testCase.modalities.hasUdfps) { assertThat(hint).isNull() } else { assertThat(hint.isNullOrBlank()).isTrue() } } @Test Loading