From 4a0a2addc90dadcc214f2fd35a000443fb268096 Mon Sep 17 00:00:00 2001 From: Beverly Date: Thu, 20 Jul 2023 19:23:56 +0000 Subject: [PATCH] Add face auth a11y action to bouncer view Test: atest FaceAuthAccessibilityDelegateTest KeyguardSecurityContainerControllerTest Test: manually enable switch access and see user has a way to retrigger face auth using switch access (tested with and without new face auth refactor) Bug: 289162753 Change-Id: I42a77f815ec90dab60a3c585a63993e2e1e579d9 --- packages/SystemUI/res/values/strings.xml | 3 + .../com/android/keyguard/FaceAuthReason.kt | 8 +- .../KeyguardSecurityContainerController.java | 3 + .../keyguard/KeyguardUpdateMonitor.java | 4 + .../FaceAuthAccessibilityDelegate.kt | 62 ++++++++++ .../interactor/KeyguardFaceAuthInteractor.kt | 1 + .../NoopKeyguardFaceAuthInteractor.kt | 1 + .../SystemUIKeyguardFaceAuthInteractor.kt | 4 + ...KeyguardSecurityContainerControllerTest.kt | 8 ++ .../FaceAuthAccessibilityDelegateTest.kt | 111 ++++++++++++++++++ 10 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f8c13b008fd99..c306a79b859fd 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2582,6 +2582,9 @@ Swipe to see more + + Retry face authentication + Loading recommendations diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index d8085b9f9f2e3..22cdb30376d06 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -20,6 +20,7 @@ import android.annotation.StringDef import android.os.PowerManager import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.keyguard.FaceAuthApiRequestReason.Companion.ACCESSIBILITY_ACTION import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED import com.android.keyguard.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED import com.android.keyguard.FaceAuthApiRequestReason.Companion.QS_EXPANDED @@ -71,6 +72,7 @@ import com.android.keyguard.InternalFaceAuthReasons.USER_SWITCHING NOTIFICATION_PANEL_CLICKED, QS_EXPANDED, PICK_UP_GESTURE_TRIGGERED, + ACCESSIBILITY_ACTION, ) annotation class FaceAuthApiRequestReason { companion object { @@ -80,6 +82,7 @@ annotation class FaceAuthApiRequestReason { const val QS_EXPANDED = "Face auth due to QS expansion." const val PICK_UP_GESTURE_TRIGGERED = "Face auth due to pickup gesture triggered when the device is awake and not from AOD." + const val ACCESSIBILITY_ACTION = "Face auth due to an accessibility action." } } @@ -217,7 +220,8 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : @UiEvent(doc = STRONG_AUTH_ALLOWED_CHANGED) FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED(1255, STRONG_AUTH_ALLOWED_CHANGED), @UiEvent(doc = NON_STRONG_BIOMETRIC_ALLOWED_CHANGED) - FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED); + FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED), + @UiEvent(doc = ACCESSIBILITY_ACTION) FACE_AUTH_ACCESSIBILITY_ACTION(1454, ACCESSIBILITY_ACTION); override fun getId(): Int = this.id @@ -233,6 +237,8 @@ private val apiRequestReasonToUiEvent = FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, QS_EXPANDED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, + PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, + ACCESSIBILITY_ACTION to FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, ) /** Converts the [reason] to the corresponding [FaceAuthUiEvent]. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index f9523370adb17..bc24249b23c91 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -68,6 +68,7 @@ import com.android.keyguard.dagger.KeyguardBouncerScope; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; +import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate; import com.android.systemui.biometrics.SideFpsController; import com.android.systemui.biometrics.SideFpsUiRequestSource; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; @@ -417,9 +418,11 @@ public class KeyguardSecurityContainerController extends ViewController javaAdapter, UserInteractor userInteractor, + FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, Provider sceneInteractor ) { super(view); + view.setAccessibilityDelegate(faceAuthAccessibilityDelegate); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = keyguardUpdateMonitor; mSecurityModel = keyguardSecurityModel; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index f5022ab64add1..f1cb37c0ae76e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -3158,6 +3158,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return false; } + if (isFaceAuthInteractorEnabled()) { + return mFaceAuthInteractor.canFaceAuthRun(); + } + final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED; final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive && !statusBarShadeLocked; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt new file mode 100644 index 0000000000000..b9fa24022ad55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import android.content.res.Resources +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo +import com.android.keyguard.FaceAuthApiRequestReason +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import javax.inject.Inject + +/** + * Accessibility delegate that will add a click accessibility action to a view when face auth can + * run. When the click a11y action is triggered, face auth will retry. + */ +@SysUISingleton +class FaceAuthAccessibilityDelegate +@Inject +constructor( + @Main private val resources: Resources, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val faceAuthInteractor: KeyguardFaceAuthInteractor, +) : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(host, info) + if (keyguardUpdateMonitor.shouldListenForFace()) { + val clickActionToRetryFace = + AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, + resources.getString(R.string.retry_face) + ) + info.addAction(clickActionToRetryFace) + } + } + + override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { + return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) { + keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION) + faceAuthInteractor.onAccessibilityAction() + true + } else super.performAccessibilityAction(host, action, args) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt index 141b13055889d..e57c919a5b3ef 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt @@ -60,6 +60,7 @@ interface KeyguardFaceAuthInteractor { fun onNotificationPanelClicked() fun onSwipeUpOnBouncer() fun onPrimaryBouncerUserInput() + fun onAccessibilityAction() } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt index 10dd900e437eb..596a1c01ca42f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt @@ -60,4 +60,5 @@ class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInt override fun onSwipeUpOnBouncer() {} override fun onPrimaryBouncerUserInput() {} + override fun onAccessibilityAction() {} } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt index 6e7a20b092f4a..2afe97d9800e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt @@ -143,6 +143,10 @@ constructor( runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false) } + override fun onAccessibilityAction() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false) + } + override fun registerListener(listener: FaceAuthenticationListener) { listeners.add(listener) } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index d44717420bdf8..3abae6bcd1975 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -37,6 +37,7 @@ import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserS import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate import com.android.systemui.biometrics.SideFpsController import com.android.systemui.biometrics.SideFpsUiRequestSource import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants @@ -123,6 +124,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var audioManager: AudioManager @Mock private lateinit var userInteractor: UserInteractor + @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Captor private lateinit var swipeListenerArgumentCaptor: @@ -224,6 +226,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mock(), { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, userInteractor, + faceAuthAccessibilityDelegate, ) { sceneInteractor } @@ -243,6 +246,11 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { ) } + @Test + fun setAccessibilityDelegate() { + verify(view).accessibilityDelegate = eq(faceAuthAccessibilityDelegate) + } + @Test fun showSecurityScreen_canInflateAllModes() { val modes = SecurityMode.values() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt new file mode 100644 index 0000000000000..ec17794d4ee21 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import android.testing.TestableLooper +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.FaceAuthApiRequestReason +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@SmallTest +@TestableLooper.RunWithLooper +class FaceAuthAccessibilityDelegateTest : SysuiTestCase() { + + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var hostView: View + @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + private lateinit var underTest: FaceAuthAccessibilityDelegate + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + FaceAuthAccessibilityDelegate( + context.resources, + keyguardUpdateMonitor, + faceAuthInteractor, + ) + } + + @Test + fun shouldListenForFaceTrue_onInitializeAccessibilityNodeInfo_clickActionAdded() { + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) + + // WHEN node is initialized + val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java) + underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo) + + // THEN a11y action is added + val argumentCaptor = argumentCaptor() + verify(mockedNodeInfo).addAction(argumentCaptor.capture()) + + // AND the a11y action is a click action + assertEquals( + AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, + argumentCaptor.value.id + ) + } + + @Test + fun shouldListenForFaceFalse_onInitializeAccessibilityNodeInfo_clickActionNotAdded() { + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false) + + // WHEN node is initialized + val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java) + underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo) + + // THEN a11y action is NOT added + verify(mockedNodeInfo, never()) + .addAction(any(AccessibilityNodeInfo.AccessibilityAction::class.java)) + } + + @Test + fun performAccessibilityAction_actionClick_retriesFaceAuth() { + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) + + // WHEN click action is performed + underTest.performAccessibilityAction( + hostView, + AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, + null + ) + + // THEN retry face auth + verify(keyguardUpdateMonitor) + .requestFaceAuth(eq(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION)) + verify(faceAuthInteractor).onAccessibilityAction() + } +}