Loading packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +10 −17 Original line number Diff line number Diff line Loading @@ -78,17 +78,6 @@ constructor( private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) /** * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. */ val isConfirmationRequired: Flow<Boolean> = combine(_isOverlayTouched, promptSelectorInteractor.isConfirmationRequired) { isOverlayTouched, isConfirmationRequired -> !isOverlayTouched && isConfirmationRequired } /** The kind of credential the user has. */ val credentialKind: Flow<PromptKind> = promptSelectorInteractor.credentialKind Loading Loading @@ -137,6 +126,15 @@ constructor( } .distinctUntilChanged() /** * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. Confirmation always required when in explicit flow. */ val isConfirmationRequired: Flow<Boolean> = combine(_isOverlayTouched, size) { isOverlayTouched, size -> !isOverlayTouched && size.isNotSmall } /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() Loading Loading @@ -170,12 +168,7 @@ constructor( .distinctUntilChanged() /** If the icon can be used as a confirmation button. */ val isIconConfirmButton: Flow<Boolean> = combine(size, promptSelectorInteractor.isConfirmationRequired) { size, isConfirmationRequired -> size.isNotSmall && isConfirmationRequired } val isIconConfirmButton: Flow<Boolean> = size.map { it.isNotSmall }.distinctUntilChanged() /** If the negative button should be shown. */ val isNegativeButtonVisible: Flow<Boolean> = Loading packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +80 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.hardware.biometrics.PromptInfo import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.view.HapticFeedbackConstants import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase Loading Loading @@ -499,6 +500,81 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(canTryAgain).isFalse() } @Test fun auto_confirm_authentication_when_finger_down() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) // No icon button when face only, can't confirm before auth if (!testCase.isFaceOnly) { viewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_DOWN)) } viewModel.showAuthenticated(testCase.authenticatedModality, 0) val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticating).isFalse() assertThat(canTryAgain).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() if (testCase.isFaceOnly && expectConfirmation) { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) assertThat(size).isEqualTo(PromptSize.MEDIUM) assertButtonsVisible( cancel = true, confirm = true, ) viewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) assertButtonsVisible() } else { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) } } @Test fun cannot_auto_confirm_authentication_when_finger_up() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) // No icon button when face only, can't confirm before auth if (!testCase.isFaceOnly) { viewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_DOWN)) viewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_UP)) } viewModel.showAuthenticated(testCase.authenticatedModality, 0) val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) if (expectConfirmation) { assertThat(size).isEqualTo(PromptSize.MEDIUM) assertButtonsVisible( cancel = true, confirm = true, ) viewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) assertButtonsVisible() } assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) assertThat(canTryAgain).isFalse() } @Test fun cannot_confirm_unless_authenticated() = runGenericTest { val authenticating by collectLastValue(viewModel.isAuthenticating) Loading Loading @@ -679,6 +755,10 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa testScope.runTest { block() } } /** Obtain a MotionEvent with the specified MotionEvent action constant */ private fun obtainMotionEvent(action: Int): MotionEvent = MotionEvent.obtain(0, 0, action, 0f, 0f, 0) companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") Loading Loading
packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +10 −17 Original line number Diff line number Diff line Loading @@ -78,17 +78,6 @@ constructor( private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) /** * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. */ val isConfirmationRequired: Flow<Boolean> = combine(_isOverlayTouched, promptSelectorInteractor.isConfirmationRequired) { isOverlayTouched, isConfirmationRequired -> !isOverlayTouched && isConfirmationRequired } /** The kind of credential the user has. */ val credentialKind: Flow<PromptKind> = promptSelectorInteractor.credentialKind Loading Loading @@ -137,6 +126,15 @@ constructor( } .distinctUntilChanged() /** * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. Confirmation always required when in explicit flow. */ val isConfirmationRequired: Flow<Boolean> = combine(_isOverlayTouched, size) { isOverlayTouched, size -> !isOverlayTouched && size.isNotSmall } /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() Loading Loading @@ -170,12 +168,7 @@ constructor( .distinctUntilChanged() /** If the icon can be used as a confirmation button. */ val isIconConfirmButton: Flow<Boolean> = combine(size, promptSelectorInteractor.isConfirmationRequired) { size, isConfirmationRequired -> size.isNotSmall && isConfirmationRequired } val isIconConfirmButton: Flow<Boolean> = size.map { it.isNotSmall }.distinctUntilChanged() /** If the negative button should be shown. */ val isNegativeButtonVisible: Flow<Boolean> = Loading
packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +80 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.hardware.biometrics.PromptInfo import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.view.HapticFeedbackConstants import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase Loading Loading @@ -499,6 +500,81 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(canTryAgain).isFalse() } @Test fun auto_confirm_authentication_when_finger_down() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) // No icon button when face only, can't confirm before auth if (!testCase.isFaceOnly) { viewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_DOWN)) } viewModel.showAuthenticated(testCase.authenticatedModality, 0) val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticating).isFalse() assertThat(canTryAgain).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() if (testCase.isFaceOnly && expectConfirmation) { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION) assertThat(size).isEqualTo(PromptSize.MEDIUM) assertButtonsVisible( cancel = true, confirm = true, ) viewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) assertButtonsVisible() } else { assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) } } @Test fun cannot_auto_confirm_authentication_when_finger_up() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) // No icon button when face only, can't confirm before auth if (!testCase.isFaceOnly) { viewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_DOWN)) viewModel.onOverlayTouch(obtainMotionEvent(MotionEvent.ACTION_UP)) } viewModel.showAuthenticated(testCase.authenticatedModality, 0) val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) if (expectConfirmation) { assertThat(size).isEqualTo(PromptSize.MEDIUM) assertButtonsVisible( cancel = true, confirm = true, ) viewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) assertButtonsVisible() } assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED) assertThat(canTryAgain).isFalse() } @Test fun cannot_confirm_unless_authenticated() = runGenericTest { val authenticating by collectLastValue(viewModel.isAuthenticating) Loading Loading @@ -679,6 +755,10 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa testScope.runTest { block() } } /** Obtain a MotionEvent with the specified MotionEvent action constant */ private fun obtainMotionEvent(action: Int): MotionEvent = MotionEvent.obtain(0, 0, action, 0f, 0f, 0) companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") Loading