Migrate haptic to view model and out of legacy components.

Fix: 272832355
Bug: 288175645
Test: atest PromptViewModelTest AuthContainerViewTest AuthControllerTest
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:0b64d441089361fa000c09bfaa2e99b0efcfc012)
Merged-In: Ie7181b448b74e4f7322cfad59eced1ac872a9b85
Change-Id: Ie7181b448b74e4f7322cfad59eced1ac872a9b85
This commit is contained in:
Joe Bolinger
2023-06-24 00:56:38 +00:00
committed by Cherrypicker Worker
parent a5aa965b2b
commit 3b9e125c9f
10 changed files with 99 additions and 72 deletions

View File

@@ -737,7 +737,7 @@ public abstract class AuthBiometricView extends LinearLayout implements AuthBiom
}); });
mUseCredentialButton.setOnClickListener((view) -> { mUseCredentialButton.setOnClickListener((view) -> {
startTransitionToCredentialUI(); startTransitionToCredentialUI(false /* isError */);
}); });
mConfirmButton.setOnClickListener((view) -> { mConfirmButton.setOnClickListener((view) -> {
@@ -768,9 +768,12 @@ public abstract class AuthBiometricView extends LinearLayout implements AuthBiom
/** /**
* Kicks off the animation process and invokes the callback. * Kicks off the animation process and invokes the callback.
*
* @param isError if this was triggered due to an error and not a user action (unused,
* previously for haptics).
*/ */
@Override @Override
public void startTransitionToCredentialUI() { public void startTransitionToCredentialUI(boolean isError) {
updateSize(AuthDialog.SIZE_LARGE); updateSize(AuthDialog.SIZE_LARGE);
mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL); mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
} }

View File

@@ -38,7 +38,7 @@ interface AuthBiometricViewAdapter {
fun onHelp(@BiometricAuthenticator.Modality modality: Int, help: String) fun onHelp(@BiometricAuthenticator.Modality modality: Int, help: String)
fun startTransitionToCredentialUI() fun startTransitionToCredentialUI(isError: Boolean)
fun requestLayout() fun requestLayout()

View File

@@ -801,9 +801,9 @@ public class AuthContainerView extends LinearLayout
} }
@Override @Override
public void animateToCredentialUI() { public void animateToCredentialUI(boolean isError) {
if (mBiometricView != null) { if (mBiometricView != null) {
mBiometricView.startTransitionToCredentialUI(); mBiometricView.startTransitionToCredentialUI(isError);
} else { } else {
Log.e(TAG, "animateToCredentialUI(): mBiometricView is null"); Log.e(TAG, "animateToCredentialUI(): mBiometricView is null");
} }

View File

@@ -85,7 +85,6 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.data.repository.BiometricType; import com.android.systemui.keyguard.data.repository.BiometricType;
import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.Execution; import com.android.systemui.util.concurrency.Execution;
@@ -185,18 +184,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
private final @Background DelayableExecutor mBackgroundExecutor; private final @Background DelayableExecutor mBackgroundExecutor;
private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
private final VibratorHelper mVibratorHelper;
private void vibrateSuccess(int modality) {
mVibratorHelper.vibrateAuthSuccess(
getClass().getSimpleName() + ", modality = " + modality + "BP::success");
}
private void vibrateError(int modality) {
mVibratorHelper.vibrateAuthError(
getClass().getSimpleName() + ", modality = " + modality + "BP::error");
}
@VisibleForTesting @VisibleForTesting
final TaskStackListener mTaskStackListener = new TaskStackListener() { final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override @Override
@@ -776,7 +763,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
@NonNull InteractionJankMonitor jankMonitor, @NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler, @Main Handler handler,
@Background DelayableExecutor bgExecutor, @Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibrator,
@NonNull UdfpsUtils udfpsUtils) { @NonNull UdfpsUtils udfpsUtils) {
mContext = context; mContext = context;
mFeatureFlags = featureFlags; mFeatureFlags = featureFlags;
@@ -798,7 +784,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
mUdfpsEnrolledForUser = new SparseBooleanArray(); mUdfpsEnrolledForUser = new SparseBooleanArray();
mSfpsEnrolledForUser = new SparseBooleanArray(); mSfpsEnrolledForUser = new SparseBooleanArray();
mFaceEnrolledForUser = new SparseBooleanArray(); mFaceEnrolledForUser = new SparseBooleanArray();
mVibratorHelper = vibrator;
mUdfpsUtils = udfpsUtils; mUdfpsUtils = udfpsUtils;
mApplicationCoroutineScope = applicationCoroutineScope; mApplicationCoroutineScope = applicationCoroutineScope;
@@ -987,8 +972,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
public void onBiometricAuthenticated(@Modality int modality) { public void onBiometricAuthenticated(@Modality int modality) {
if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: "); if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: ");
vibrateSuccess(modality);
if (mCurrentDialog != null) { if (mCurrentDialog != null) {
mCurrentDialog.onAuthenticationSucceeded(modality); mCurrentDialog.onAuthenticationSucceeded(modality);
} else { } else {
@@ -1085,8 +1068,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode)); Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode));
} }
vibrateError(modality);
final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT) final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT)
|| (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT); || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT);
@@ -1103,7 +1084,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
if (mCurrentDialog != null) { if (mCurrentDialog != null) {
if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) { if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) {
if (DEBUG) Log.d(TAG, "onBiometricError, lockout"); if (DEBUG) Log.d(TAG, "onBiometricError, lockout");
mCurrentDialog.animateToCredentialUI(); mCurrentDialog.animateToCredentialUI(true /* isError */);
} else if (isSoftError) { } else if (isSoftError) {
final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED) final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED)
? getNotRecognizedString(modality) ? getNotRecognizedString(modality)

View File

@@ -162,7 +162,7 @@ public interface AuthDialog extends Dumpable {
/** /**
* Animate to credential UI. Typically called after biometric is locked out. * Animate to credential UI. Typically called after biometric is locked out.
*/ */
void animateToCredentialUI(); void animateToCredentialUI(boolean isError);
/** /**
* @return true if device credential is allowed. * @return true if device credential is allowed.

View File

@@ -522,6 +522,7 @@ private class Spaghetti(
viewModel.showTemporaryError( viewModel.showTemporaryError(
help, help,
messageAfterError = modalities.asDefaultHelpMessage(applicationContext), messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
hapticFeedback = false,
) )
} }
} }
@@ -534,7 +535,7 @@ private class Spaghetti(
else -> false else -> false
} }
override fun startTransitionToCredentialUI() { override fun startTransitionToCredentialUI(isError: Boolean) {
applicationScope.launch { applicationScope.launch {
viewModel.onSwitchToCredential() viewModel.onSwitchToCredential()
legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL) legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)

View File

@@ -22,6 +22,7 @@ import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteracto
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.domain.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 javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@@ -41,6 +42,7 @@ class PromptViewModel
@Inject @Inject
constructor( constructor(
private val interactor: PromptSelectorInteractor, private val interactor: PromptSelectorInteractor,
private val vibrator: VibratorHelper,
) { ) {
/** The set of modalities available for this prompt */ /** The set of modalities available for this prompt */
val modalities: Flow<BiometricModalities> = val modalities: Flow<BiometricModalities> =
@@ -205,17 +207,19 @@ constructor(
private var messageJob: Job? = null private var messageJob: Job? = null
/** /**
* Show a temporary error [message] associated with an optional [failedModality]. * Show a temporary error [message] associated with an optional [failedModality] and play
* [hapticFeedback].
* *
* An optional [messageAfterError] will be shown via [showAuthenticating] when * An optional [messageAfterError] will be shown via [showAuthenticating] when
* [authenticateAfterError] is set (or via [showHelp] when not set) after the error is * [authenticateAfterError] is set (or via [showHelp] when not set) after the error is
* dismissed. * dismissed.
* *
* The error is ignored if the user has already authenticated and it is treated as * The error is ignored if the user has already authenticated or if [suppressIfErrorShowing] is
* [onSilentError] if [suppressIfErrorShowing] is set and an error message is already showing. * set and an error message is already showing.
*/ */
suspend fun showTemporaryError( suspend fun showTemporaryError(
message: String, message: String,
hapticFeedback: Boolean = true,
messageAfterError: String = "", messageAfterError: String = "",
authenticateAfterError: Boolean = false, authenticateAfterError: Boolean = false,
suppressIfErrorShowing: Boolean = false, suppressIfErrorShowing: Boolean = false,
@@ -225,7 +229,9 @@ constructor(
return@coroutineScope return@coroutineScope
} }
if (_message.value.isErrorOrHelp && suppressIfErrorShowing) { if (_message.value.isErrorOrHelp && suppressIfErrorShowing) {
onSilentError(failedModality) if (_isAuthenticated.value.isNotAuthenticated) {
_canTryAgainNow.value = supportsRetry(failedModality)
}
return@coroutineScope return@coroutineScope
} }
@@ -236,6 +242,10 @@ constructor(
_message.value = PromptMessage.Error(message) _message.value = PromptMessage.Error(message)
_legacyState.value = AuthBiometricView.STATE_ERROR _legacyState.value = AuthBiometricView.STATE_ERROR
if (hapticFeedback) {
vibrator.error(failedModality)
}
messageJob?.cancel() messageJob?.cancel()
messageJob = launch { messageJob = launch {
delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong()) delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
@@ -247,18 +257,6 @@ constructor(
} }
} }
/**
* Call instead of [showTemporaryError] if an error from the HAL should be silently ignored to
* enable retry (if the [failedModality] supports retrying).
*
* Ignored if the user has already authenticated.
*/
private fun onSilentError(failedModality: BiometricModality = BiometricModality.None) {
if (_isAuthenticated.value.isNotAuthenticated) {
_canTryAgainNow.value = supportsRetry(failedModality)
}
}
/** /**
* Call to ensure the fingerprint sensor has started. Either when the dialog is first shown * Call to ensure the fingerprint sensor has started. Either when the dialog is first shown
* (most cases) or when it should be enabled after a first error (coex implicit flow). * (most cases) or when it should be enabled after a first error (coex implicit flow).
@@ -376,6 +374,8 @@ constructor(
AuthBiometricView.STATE_AUTHENTICATED AuthBiometricView.STATE_AUTHENTICATED
} }
vibrator.success(modality)
messageJob?.cancel() messageJob?.cancel()
messageJob = null messageJob = null
@@ -386,18 +386,18 @@ constructor(
private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean { private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
val availableModalities = modalities.first() val availableModalities = modalities.first()
val confirmationRequested = interactor.isConfirmationRequired.first() val confirmationRequired = isConfirmationRequired.first()
if (availableModalities.hasFaceAndFingerprint) { if (availableModalities.hasFaceAndFingerprint) {
// coex only needs confirmation when face is successful, unless it happens on the // coex only needs confirmation when face is successful, unless it happens on the
// first attempt (i.e. without failure) before fingerprint scanning starts // first attempt (i.e. without failure) before fingerprint scanning starts
val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending
if (modality == BiometricModality.Face) { if (modality == BiometricModality.Face) {
return (fingerprintStartMode.first() != FingerprintStartMode.Pending) || return fingerprintStarted || confirmationRequired
confirmationRequested
} }
} }
if (availableModalities.hasFaceOnly) { if (availableModalities.hasFaceOnly) {
return confirmationRequested return confirmationRequired
} }
// fingerprint only never requires confirmation // fingerprint only never requires confirmation
return false return false
@@ -412,7 +412,6 @@ constructor(
fun confirmAuthenticated() { fun confirmAuthenticated() {
val authState = _isAuthenticated.value val authState = _isAuthenticated.value
if (authState.isNotAuthenticated) { if (authState.isNotAuthenticated) {
"Cannot show authenticated after authenticated"
Log.w(TAG, "Cannot confirm authenticated when not authenticated") Log.w(TAG, "Cannot confirm authenticated when not authenticated")
return return
} }
@@ -434,6 +433,12 @@ constructor(
_forceLargeSize.value = true _forceLargeSize.value = true
} }
private fun VibratorHelper.success(modality: BiometricModality) =
vibrateAuthSuccess("$TAG, modality = $modality BP::success")
private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) =
vibrateAuthError("$TAG, modality = $modality BP::error")
companion object { companion object {
private const val TAG = "PromptViewModel" private const val TAG = "PromptViewModel"
} }

View File

@@ -51,6 +51,7 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -99,6 +100,8 @@ open class AuthContainerViewTest : SysuiTestCase() {
lateinit var windowToken: IBinder lateinit var windowToken: IBinder
@Mock @Mock
lateinit var interactionJankMonitor: InteractionJankMonitor lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock
lateinit var vibrator: VibratorHelper
// TODO(b/278622168): remove with flag // TODO(b/278622168): remove with flag
open val useNewBiometricPrompt = false open val useNewBiometricPrompt = false
@@ -325,7 +328,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL BiometricManager.Authenticators.DEVICE_CREDENTIAL
) )
container.animateToCredentialUI() container.animateToCredentialUI(false)
waitForIdleSync() waitForIdleSync()
assertThat(container.hasCredentialView()).isTrue() assertThat(container.hasCredentialView()).isTrue()
@@ -514,7 +517,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
{ authBiometricFingerprintViewModel }, { authBiometricFingerprintViewModel },
{ promptSelectorInteractor }, { promptSelectorInteractor },
{ bpCredentialInteractor }, { bpCredentialInteractor },
PromptViewModel(promptSelectorInteractor), PromptViewModel(promptSelectorInteractor, vibrator),
{ credentialViewModel }, { credentialViewModel },
Handler(TestableLooper.get(this).looper), Handler(TestableLooper.get(this).looper),
fakeExecutor fakeExecutor

View File

@@ -202,9 +202,6 @@ public class AuthControllerTest extends SysuiTestCase {
private TestableAuthController mAuthController; private TestableAuthController mAuthController;
private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private VibratorHelper mVibratorHelper;
@Before @Before
public void setup() throws RemoteException { public void setup() throws RemoteException {
// TODO(b/278622168): remove with flag // TODO(b/278622168): remove with flag
@@ -267,7 +264,6 @@ public class AuthControllerTest extends SysuiTestCase {
true /* supportsSelfIllumination */, true /* supportsSelfIllumination */,
true /* resetLockoutRequireHardwareAuthToken */)); true /* resetLockoutRequireHardwareAuthToken */));
when(mFaceManager.getSensorPropertiesInternal()).thenReturn(faceProps); when(mFaceManager.getSensorPropertiesInternal()).thenReturn(faceProps);
when(mVibratorHelper.hasVibrator()).thenReturn(true);
mAuthController = new TestableAuthController(mContextSpy); mAuthController = new TestableAuthController(mContextSpy);
@@ -550,7 +546,7 @@ public class AuthControllerTest extends SysuiTestCase {
mAuthController.onBiometricError(BiometricAuthenticator.TYPE_FACE, error, vendorCode); mAuthController.onBiometricError(BiometricAuthenticator.TYPE_FACE, error, vendorCode);
verify(mDialog1, never()).onError(anyInt(), anyString()); verify(mDialog1, never()).onError(anyInt(), anyString());
verify(mDialog1).animateToCredentialUI(); verify(mDialog1).animateToCredentialUI(eq(true));
} }
@Test @Test
@@ -563,7 +559,7 @@ public class AuthControllerTest extends SysuiTestCase {
mAuthController.onBiometricError(BiometricAuthenticator.TYPE_FACE, error, vendorCode); mAuthController.onBiometricError(BiometricAuthenticator.TYPE_FACE, error, vendorCode);
verify(mDialog1, never()).onError(anyInt(), anyString()); verify(mDialog1, never()).onError(anyInt(), anyString());
verify(mDialog1).animateToCredentialUI(); verify(mDialog1).animateToCredentialUI(eq(true));
} }
@Test @Test
@@ -578,7 +574,7 @@ public class AuthControllerTest extends SysuiTestCase {
mAuthController.onBiometricError(modality, error, vendorCode); mAuthController.onBiometricError(modality, error, vendorCode);
verify(mDialog1).onError( verify(mDialog1).onError(
eq(modality), eq(FaceManager.getErrorString(mContext, error, vendorCode))); eq(modality), eq(FaceManager.getErrorString(mContext, error, vendorCode)));
verify(mDialog1, never()).animateToCredentialUI(); verify(mDialog1, never()).animateToCredentialUI(eq(true));
} }
@Test @Test
@@ -593,7 +589,7 @@ public class AuthControllerTest extends SysuiTestCase {
mAuthController.onBiometricError(modality, error, vendorCode); mAuthController.onBiometricError(modality, error, vendorCode);
verify(mDialog1).onError( verify(mDialog1).onError(
eq(modality), eq(FaceManager.getErrorString(mContext, error, vendorCode))); eq(modality), eq(FaceManager.getErrorString(mContext, error, vendorCode)));
verify(mDialog1, never()).animateToCredentialUI(); verify(mDialog1, never()).animateToCredentialUI(eq(true));
} }
@Test @Test
@@ -1012,7 +1008,7 @@ public class AuthControllerTest extends SysuiTestCase {
() -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor, () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
() -> mCredentialViewModel, () -> mPromptViewModel, () -> mCredentialViewModel, () -> mPromptViewModel,
mInteractionJankMonitor, mHandler, mInteractionJankMonitor, mHandler,
mBackgroundExecutor, mVibratorHelper, mUdfpsUtils); mBackgroundExecutor, mUdfpsUtils);
} }
@Override @Override

View File

@@ -32,6 +32,8 @@ 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.coroutines.collectLastValue import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -45,6 +47,9 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
private const val USER_ID = 4 private const val USER_ID = 4
@@ -58,6 +63,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
@JvmField @Rule var mockitoRule = MockitoJUnit.rule() @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
@Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var vibrator: VibratorHelper
private val testScope = TestScope() private val testScope = TestScope()
private val promptRepository = FakePromptRepository() private val promptRepository = FakePromptRepository()
@@ -70,11 +76,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils) selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
selector.resetPrompt() selector.resetPrompt()
viewModel = PromptViewModel(selector) viewModel = PromptViewModel(selector, vibrator)
} }
@Test @Test
fun `start idle and show authenticating`() = fun start_idle_and_show_authenticating() =
runGenericTest(doNotStart = true) { runGenericTest(doNotStart = true) {
val expectedSize = val expectedSize =
if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM
@@ -107,7 +113,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `shows authenticated - no errors`() = runGenericTest { fun shows_authenticated_with_no_errors() = runGenericTest {
// this case can't happen until fingerprint is started // this case can't happen until fingerprint is started
// trigger it now since no error has occurred in this test // trigger it now since no error has occurred in this test
val forceError = testCase.isCoex && testCase.authenticatedByFingerprint val forceError = testCase.isCoex && testCase.authenticatedByFingerprint
@@ -124,6 +130,22 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
) )
} }
@Test
fun plays_haptic_on_authenticated() = runGenericTest {
viewModel.showAuthenticated(testCase.authenticatedModality, 1000L)
verify(vibrator).vibrateAuthSuccess(any())
verify(vibrator, never()).vibrateAuthError(any())
}
@Test
fun plays_no_haptic_on_confirm() = runGenericTest {
viewModel.confirmAuthenticated()
verify(vibrator, never()).vibrateAuthSuccess(any())
verify(vibrator, never()).vibrateAuthError(any())
}
private suspend fun TestScope.showAuthenticated( private suspend fun TestScope.showAuthenticated(
authenticatedModality: BiometricModality, authenticatedModality: BiometricModality,
expectConfirmation: Boolean, expectConfirmation: Boolean,
@@ -172,7 +194,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `shows temporary errors`() = runGenericTest { fun shows_temporary_errors() = runGenericTest {
val checkAtEnd = suspend { assertButtonsVisible(negative = true) } val checkAtEnd = suspend { assertButtonsVisible(negative = true) }
showTemporaryErrors(restart = false) { checkAtEnd() } showTemporaryErrors(restart = false) { checkAtEnd() }
@@ -180,6 +202,22 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
showTemporaryErrors(restart = true) { checkAtEnd() } showTemporaryErrors(restart = true) { checkAtEnd() }
} }
@Test
fun plays_haptic_on_errors() = runGenericTest {
viewModel.showTemporaryError("so sad", hapticFeedback = true)
verify(vibrator).vibrateAuthError(any())
verify(vibrator, never()).vibrateAuthSuccess(any())
}
@Test
fun plays_haptic_on_errors_unless_skipped() = runGenericTest {
viewModel.showTemporaryError("still sad", hapticFeedback = false)
verify(vibrator, never()).vibrateAuthError(any())
verify(vibrator, never()).vibrateAuthSuccess(any())
}
private suspend fun TestScope.showTemporaryErrors( private suspend fun TestScope.showTemporaryErrors(
restart: Boolean, restart: Boolean,
helpAfterError: String = "", helpAfterError: String = "",
@@ -233,7 +271,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `no errors or temporary help after authenticated`() = runGenericTest { fun no_errors_or_temporary_help_after_authenticated() = runGenericTest {
val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticating by collectLastValue(viewModel.isAuthenticating)
val authenticated by collectLastValue(viewModel.isAuthenticated) val authenticated by collectLastValue(viewModel.isAuthenticated)
val message by collectLastValue(viewModel.message) val message by collectLastValue(viewModel.message)
@@ -277,7 +315,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `authenticated at most once`() = runGenericTest { fun authenticated_at_most_once() = runGenericTest {
val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticating by collectLastValue(viewModel.isAuthenticating)
val authenticated by collectLastValue(viewModel.isAuthenticated) val authenticated by collectLastValue(viewModel.isAuthenticated)
@@ -293,7 +331,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `authenticating cannot restart after authenticated`() = runGenericTest { fun authenticating_cannot_restart_after_authenticated() = runGenericTest {
val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticating by collectLastValue(viewModel.isAuthenticating)
val authenticated by collectLastValue(viewModel.isAuthenticated) val authenticated by collectLastValue(viewModel.isAuthenticated)
@@ -309,7 +347,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `confirm authentication`() = runGenericTest { fun confirm_authentication() = runGenericTest {
val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
viewModel.showAuthenticated(testCase.authenticatedModality, 0) viewModel.showAuthenticated(testCase.authenticatedModality, 0)
@@ -341,7 +379,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `cannot confirm unless authenticated`() = runGenericTest { fun cannot_confirm_unless_authenticated() = runGenericTest {
val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticating by collectLastValue(viewModel.isAuthenticating)
val authenticated by collectLastValue(viewModel.isAuthenticated) val authenticated by collectLastValue(viewModel.isAuthenticated)
@@ -360,7 +398,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `shows help - before authenticated`() = runGenericTest { fun shows_help_before_authenticated() = runGenericTest {
val helpMessage = "please help yourself to some cookies" val helpMessage = "please help yourself to some cookies"
val message by collectLastValue(viewModel.message) val message by collectLastValue(viewModel.message)
val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
@@ -379,7 +417,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `shows help - after authenticated`() = runGenericTest { fun shows_help_after_authenticated() = runGenericTest {
val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
val helpMessage = "more cookies please" val helpMessage = "more cookies please"
val authenticating by collectLastValue(viewModel.isAuthenticating) val authenticating by collectLastValue(viewModel.isAuthenticating)
@@ -409,7 +447,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `retries after failure`() = runGenericTest { fun retries_after_failure() = runGenericTest {
val errorMessage = "bad" val errorMessage = "bad"
val helpMessage = "again?" val helpMessage = "again?"
val expectTryAgainButton = testCase.isFaceOnly val expectTryAgainButton = testCase.isFaceOnly
@@ -455,7 +493,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
} }
@Test @Test
fun `switch to credential fallback`() = runGenericTest { fun switch_to_credential_fallback() = runGenericTest {
val size by collectLastValue(viewModel.size) val size by collectLastValue(viewModel.size)
// TODO(b/251476085): remove Spaghetti, migrate logic, and update this test // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test