Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d0ac6337 authored by Grace Cheng's avatar Grace Cheng
Browse files

Clear UDFPS BiometricPrompt a11y messages after read by talkback

Clears UDFPS guidance messages after focus moves to the next view, in
order to prevent stale messages during any subsequent linear a11y
navigation, and to allow sending the same message multiple times in
succession (e.g. "Move left" twice in a row)

Flag: NONE bug fix
Fixes: 404940015
Fixes: 404938119
Fixes: 383230658
Test: (manual) verified Talkback reads expected messages
Test: atest PromptViewModelTest
Change-Id: I685ae0abe2c8df017c06187252eb8e26d56d3113
parent 1d01dbd0
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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"
+1 −1
Original line number Diff line number Diff line
@@ -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"
+37 −23
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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"

@@ -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() {
@@ -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
                                ),
                            )
                        )
                    }
@@ -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()

@@ -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
                    }
                }

+15 −2
Original line number Diff line number Diff line
@@ -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)

@@ -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.
     *
+8 −0
Original line number Diff line number Diff line
@@ -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