Merge changes from topic "cherrypicker-L01600000961894751:N92400001386673229" into udc-d1-dev

* changes:
  Move haptic from authenticated to confirmed (when required).
  Prioritize fingerprint errors for coex.
This commit is contained in:
TreeHugger Robot
2023-07-12 03:38:45 +00:00
committed by Android (Google) Code Review
7 changed files with 126 additions and 56 deletions

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.biometrics.domain.model package com.android.systemui.biometrics.shared.model
import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricAuthenticator

View File

@@ -46,9 +46,9 @@ import com.android.systemui.biometrics.AuthIconController
import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.Utils
import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.domain.model.BiometricModalities
import com.android.systemui.biometrics.domain.model.BiometricModality import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.biometrics.domain.model.asBiometricModality
import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.biometrics.shared.model.asBiometricModality
import com.android.systemui.biometrics.ui.BiometricPromptLayout import com.android.systemui.biometrics.ui.BiometricPromptLayout
import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
import com.android.systemui.biometrics.ui.viewmodel.PromptMessage import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
@@ -396,7 +396,6 @@ private class Spaghetti(
private var lifecycleScope: CoroutineScope? = null private var lifecycleScope: CoroutineScope? = null
private var modalities: BiometricModalities = BiometricModalities() private var modalities: BiometricModalities = BiometricModalities()
private var faceFailedAtLeastOnce = false
private var legacyCallback: Callback? = null private var legacyCallback: Callback? = null
override var legacyIconController: AuthIconController? = null override var legacyIconController: AuthIconController? = null
@@ -476,19 +475,15 @@ private class Spaghetti(
viewModel.ensureFingerprintHasStarted(isDelayed = true) viewModel.ensureFingerprintHasStarted(isDelayed = true)
applicationScope.launch { applicationScope.launch {
val suppress =
modalities.hasFaceAndFingerprint &&
(failedModality == BiometricModality.Face) &&
faceFailedAtLeastOnce
if (failedModality == BiometricModality.Face) {
faceFailedAtLeastOnce = true
}
viewModel.showTemporaryError( viewModel.showTemporaryError(
failureReason, failureReason,
messageAfterError = modalities.asDefaultHelpMessage(applicationContext), messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
authenticateAfterError = modalities.hasFingerprint, authenticateAfterError = modalities.hasFingerprint,
suppressIfErrorShowing = suppress, suppressIf = { currentMessage ->
modalities.hasFaceAndFingerprint &&
failedModality == BiometricModality.Face &&
currentMessage.isError
},
failedModality = failedModality, failedModality = failedModality,
) )
} }
@@ -501,11 +496,10 @@ private class Spaghetti(
} }
applicationScope.launch { applicationScope.launch {
val suppress =
modalities.hasFaceAndFingerprint && (errorModality == BiometricModality.Face)
viewModel.showTemporaryError( viewModel.showTemporaryError(
error, error,
suppressIfErrorShowing = suppress, messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
authenticateAfterError = modalities.hasFingerprint,
) )
delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong()) delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
legacyCallback?.onAction(Callback.ACTION_ERROR) legacyCallback?.onAction(Callback.ACTION_ERROR)
@@ -522,6 +516,7 @@ private class Spaghetti(
viewModel.showTemporaryError( viewModel.showTemporaryError(
help, help,
messageAfterError = modalities.asDefaultHelpMessage(applicationContext), messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
authenticateAfterError = modalities.hasFingerprint,
hapticFeedback = false, hapticFeedback = false,
) )
} }

View File

@@ -16,7 +16,7 @@
package com.android.systemui.biometrics.ui.viewmodel package com.android.systemui.biometrics.ui.viewmodel
import com.android.systemui.biometrics.domain.model.BiometricModality import com.android.systemui.biometrics.shared.model.BiometricModality
/** /**
* The authenticated state with the [authenticatedModality] (when [isAuthenticated]) with an * The authenticated state with the [authenticatedModality] (when [isAuthenticated]) with an

View File

@@ -33,9 +33,9 @@ sealed interface PromptMessage {
else -> "" else -> ""
} }
/** If this is an [Error] or [Help] message. */ /** If this is an [Error]. */
val isErrorOrHelp: Boolean val isError: Boolean
get() = this is Error || this is Help get() = this is Error
/** An error message. */ /** An error message. */
data class Error(val errorMessage: String) : PromptMessage data class Error(val errorMessage: String) : PromptMessage

View File

@@ -20,7 +20,7 @@ import android.util.Log
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
import com.android.systemui.biometrics.domain.model.BiometricModality import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper
import javax.inject.Inject import javax.inject.Inject
@@ -210,35 +210,33 @@ constructor(
* Show a temporary error [message] associated with an optional [failedModality] and play * Show a temporary error [message] associated with an optional [failedModality] and play
* [hapticFeedback]. * [hapticFeedback].
* *
* An optional [messageAfterError] will be shown via [showAuthenticating] when * The [messageAfterError] will be shown via [showAuthenticating] when [authenticateAfterError]
* [authenticateAfterError] is set (or via [showHelp] when not set) after the error is * is set (or via [showHelp] when not set) after the error is dismissed.
* dismissed.
* *
* The error is ignored if the user has already authenticated or if [suppressIfErrorShowing] is * The error is ignored if the user has already authenticated or if [suppressIf] is true given
* set and an error message is already showing. * the currently showing [PromptMessage].
*/ */
suspend fun showTemporaryError( suspend fun showTemporaryError(
message: String, message: String,
messageAfterError: String,
authenticateAfterError: Boolean,
suppressIf: (PromptMessage) -> Boolean = { false },
hapticFeedback: Boolean = true, hapticFeedback: Boolean = true,
messageAfterError: String = "",
authenticateAfterError: Boolean = false,
suppressIfErrorShowing: Boolean = false,
failedModality: BiometricModality = BiometricModality.None, failedModality: BiometricModality = BiometricModality.None,
) = coroutineScope { ) = coroutineScope {
if (_isAuthenticated.value.isAuthenticated) { if (_isAuthenticated.value.isAuthenticated) {
return@coroutineScope return@coroutineScope
} }
if (_message.value.isErrorOrHelp && suppressIfErrorShowing) {
if (_isAuthenticated.value.isNotAuthenticated) {
_canTryAgainNow.value = supportsRetry(failedModality) _canTryAgainNow.value = supportsRetry(failedModality)
}
if (suppressIf(_message.value)) {
return@coroutineScope return@coroutineScope
} }
_isAuthenticating.value = false _isAuthenticating.value = false
_isAuthenticated.value = PromptAuthState(false) _isAuthenticated.value = PromptAuthState(false)
_forceMediumSize.value = true _forceMediumSize.value = true
_canTryAgainNow.value = supportsRetry(failedModality)
_message.value = PromptMessage.Error(message) _message.value = PromptMessage.Error(message)
_legacyState.value = AuthBiometricView.STATE_ERROR _legacyState.value = AuthBiometricView.STATE_ERROR
@@ -374,7 +372,9 @@ constructor(
AuthBiometricView.STATE_AUTHENTICATED AuthBiometricView.STATE_AUTHENTICATED
} }
if (!needsUserConfirmation) {
vibrator.success(modality) vibrator.success(modality)
}
messageJob?.cancel() messageJob?.cancel()
messageJob = null messageJob = null
@@ -420,6 +420,8 @@ constructor(
_message.value = PromptMessage.Empty _message.value = PromptMessage.Empty
_legacyState.value = AuthBiometricView.STATE_AUTHENTICATED _legacyState.value = AuthBiometricView.STATE_AUTHENTICATED
vibrator.success(authState.authenticatedModality)
messageJob?.cancel() messageJob?.cancel()
messageJob = null messageJob = null
} }

View File

@@ -18,7 +18,7 @@ package com.android.systemui.biometrics.ui.viewmodel
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.domain.model.BiometricModality import com.android.systemui.biometrics.shared.model.BiometricModality
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

View File

@@ -27,11 +27,12 @@ import com.android.systemui.biometrics.data.repository.FakePromptRepository
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
import com.android.systemui.biometrics.domain.model.BiometricModalities import com.android.systemui.biometrics.domain.model.BiometricModalities
import com.android.systemui.biometrics.domain.model.BiometricModality
import com.android.systemui.biometrics.extractAuthenticatorTypes import com.android.systemui.biometrics.extractAuthenticatorTypes
import com.android.systemui.biometrics.faceSensorPropertiesInternal import com.android.systemui.biometrics.faceSensorPropertiesInternal
import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -131,18 +132,20 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun plays_haptic_on_authenticated() = runGenericTest { fun play_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() =
viewModel.showAuthenticated(testCase.authenticatedModality, 1000L) runGenericTest {
val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
verify(vibrator).vibrateAuthSuccess(any()) viewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
verify(vibrator, never()).vibrateAuthError(any())
verify(vibrator, if (expectConfirmation) never() else times(1))
.vibrateAuthSuccess(any())
if (expectConfirmation) {
viewModel.confirmAuthenticated()
} }
@Test verify(vibrator).vibrateAuthSuccess(any())
fun plays_no_haptic_on_confirm() = runGenericTest {
viewModel.confirmAuthenticated()
verify(vibrator, never()).vibrateAuthSuccess(any())
verify(vibrator, never()).vibrateAuthError(any()) verify(vibrator, never()).vibrateAuthError(any())
} }
@@ -204,7 +207,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
@Test @Test
fun plays_haptic_on_errors() = runGenericTest { fun plays_haptic_on_errors() = runGenericTest {
viewModel.showTemporaryError("so sad", hapticFeedback = true) viewModel.showTemporaryError(
"so sad",
messageAfterError = "",
authenticateAfterError = false,
hapticFeedback = true,
)
verify(vibrator).vibrateAuthError(any()) verify(vibrator).vibrateAuthError(any())
verify(vibrator, never()).vibrateAuthSuccess(any()) verify(vibrator, never()).vibrateAuthSuccess(any())
@@ -212,7 +220,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
@Test @Test
fun plays_haptic_on_errors_unless_skipped() = runGenericTest { fun plays_haptic_on_errors_unless_skipped() = runGenericTest {
viewModel.showTemporaryError("still sad", hapticFeedback = false) viewModel.showTemporaryError(
"still sad",
messageAfterError = "",
authenticateAfterError = false,
hapticFeedback = false,
)
verify(vibrator, never()).vibrateAuthError(any()) verify(vibrator, never()).vibrateAuthError(any())
verify(vibrator, never()).vibrateAuthSuccess(any()) verify(vibrator, never()).vibrateAuthSuccess(any())
@@ -287,7 +300,13 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
assertThat(canTryAgain).isFalse() assertThat(canTryAgain).isFalse()
} }
val errorJob = launch { viewModel.showTemporaryError("error") } val errorJob = launch {
viewModel.showTemporaryError(
"error",
messageAfterError = "",
authenticateAfterError = false,
)
}
verifyNoError() verifyNoError()
errorJob.join() errorJob.join()
verifyNoError() verifyNoError()
@@ -306,12 +325,66 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
assertThat(messageIsShowing).isTrue() assertThat(messageIsShowing).isTrue()
} }
// @Test @Test
fun `suppress errors`() = runGenericTest { fun suppress_temporary_error() = runGenericTest {
val errorMessage = "woot" val messages by collectValues(viewModel.message)
val message by collectLastValue(viewModel.message)
val errorJob = launch { viewModel.showTemporaryError(errorMessage) } for (error in listOf("never", "see", "me")) {
launch {
viewModel.showTemporaryError(
error,
messageAfterError = "or me",
authenticateAfterError = false,
suppressIf = { _ -> true },
)
}
}
testScheduler.advanceUntilIdle()
assertThat(messages).containsExactly(PromptMessage.Empty)
}
@Test
fun suppress_temporary_error_when_already_showing_when_requested() =
suppress_temporary_error_when_already_showing(suppress = true)
@Test
fun do_not_suppress_temporary_error_when_already_showing_when_not_requested() =
suppress_temporary_error_when_already_showing(suppress = false)
private fun suppress_temporary_error_when_already_showing(suppress: Boolean) = runGenericTest {
val errors = listOf("woot", "oh yeah", "nope")
val afterSuffix = "(after)"
val expectedErrorMessage = if (suppress) errors.first() else errors.last()
val messages by collectValues(viewModel.message)
for (error in errors) {
launch {
viewModel.showTemporaryError(
error,
messageAfterError = "$error $afterSuffix",
authenticateAfterError = false,
suppressIf = { currentMessage -> suppress && currentMessage.isError },
)
}
}
testScheduler.runCurrent()
assertThat(messages)
.containsExactly(
PromptMessage.Empty,
PromptMessage.Error(expectedErrorMessage),
)
.inOrder()
testScheduler.advanceUntilIdle()
assertThat(messages)
.containsExactly(
PromptMessage.Empty,
PromptMessage.Error(expectedErrorMessage),
PromptMessage.Help("$expectedErrorMessage $afterSuffix"),
)
.inOrder()
} }
@Test @Test