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:
committed by
Android (Google) Code Review
commit
6ca41f39ff
@@ -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
|
||||||
|
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user