Using performHapticFeedback on BiometricViewBinder

Migration towards a new oneway API to play haptics from the main thread.
A stateflow is introduced from the PromptViewModel to indicate a
HapticFeedbackConstant to be played in the BiomemtricViewBinder. The binder
collects the flow and plays the indicated haptics. CONFIRM and REJECT
constants are used to indicated success and failure of authentication. A
feature flag controls the migration.

Test: atest PromptViewModelTest
Bug: 245528624
Change-Id: I27e4057e0613ed1b24724ddafd85771da5c3019d
This commit is contained in:
Juan Sebastian Martinez
2023-08-10 15:22:53 -07:00
parent 8a9733fc8d
commit 96e79d5635
7 changed files with 117 additions and 20 deletions

View File

@@ -82,6 +82,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.PrintWriter;
@@ -288,12 +289,13 @@ public class AuthContainerView extends LinearLayout
@NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
@NonNull PromptViewModel promptViewModel,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull @Background DelayableExecutor bgExecutor) {
@NonNull @Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibratorHelper) {
this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps,
wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor,
promptCredentialInteractor, promptViewModel, credentialViewModelProvider,
new Handler(Looper.getMainLooper()), bgExecutor);
new Handler(Looper.getMainLooper()), bgExecutor, vibratorHelper);
}
@VisibleForTesting
@@ -314,7 +316,8 @@ public class AuthContainerView extends LinearLayout
@NonNull PromptViewModel promptViewModel,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull Handler mainHandler,
@NonNull @Background DelayableExecutor bgExecutor) {
@NonNull @Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibratorHelper) {
super(config.mContext);
mConfig = config;
@@ -364,7 +367,8 @@ public class AuthContainerView extends LinearLayout
if (featureFlags.isEnabled(Flags.BIOMETRIC_BP_STRONG)) {
showPrompt(config, layoutInflater, promptViewModel,
Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds),
vibratorHelper, featureFlags);
} else {
showLegacyPrompt(config, layoutInflater, fpProps, faceProps);
}
@@ -388,7 +392,10 @@ public class AuthContainerView extends LinearLayout
private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
@NonNull PromptViewModel viewModel,
@Nullable FingerprintSensorPropertiesInternal fpProps,
@Nullable FaceSensorPropertiesInternal faceProps) {
@Nullable FaceSensorPropertiesInternal faceProps,
@NonNull VibratorHelper vibratorHelper,
@NonNull FeatureFlags featureFlags
) {
if (Utils.isBiometricAllowed(config.mPromptInfo)) {
mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(
config.mPromptInfo,
@@ -401,7 +408,8 @@ public class AuthContainerView extends LinearLayout
mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
// TODO(b/201510778): This uses the wrong timeout in some cases
getJankListener(view, TRANSIT, AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
mBackgroundView, mBiometricCallback, mApplicationCoroutineScope);
mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
vibratorHelper, featureFlags);
// TODO(b/251476085): migrate these dependencies
if (fpProps != null && fpProps.isAnyUdfpsType()) {

View File

@@ -85,9 +85,12 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.data.repository.BiometricType;
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.Execution;
import kotlin.Unit;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -101,7 +104,6 @@ import java.util.Set;
import javax.inject.Inject;
import javax.inject.Provider;
import kotlin.Unit;
import kotlinx.coroutines.CoroutineScope;
/**
@@ -183,6 +185,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
@NonNull private final UdfpsUtils mUdfpsUtils;
private final @Background DelayableExecutor mBackgroundExecutor;
private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
@NonNull private final VibratorHelper mVibratorHelper;
@VisibleForTesting
final TaskStackListener mTaskStackListener = new TaskStackListener() {
@@ -771,7 +774,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
@NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
@NonNull UdfpsUtils udfpsUtils) {
@NonNull UdfpsUtils udfpsUtils,
@NonNull VibratorHelper vibratorHelper) {
mContext = context;
mFeatureFlags = featureFlags;
mExecution = execution;
@@ -794,6 +798,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
mFaceEnrolledForUser = new SparseBooleanArray();
mUdfpsUtils = udfpsUtils;
mApplicationCoroutineScope = applicationCoroutineScope;
mVibratorHelper = vibratorHelper;
mLogContextInteractor = logContextInteractor;
mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
@@ -1341,7 +1346,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider,
mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel,
mCredentialViewModelProvider, bgExecutor);
mCredentialViewModelProvider, bgExecutor, mVibratorHelper);
}
@Override

View File

@@ -25,6 +25,7 @@ import android.hardware.face.FaceManager
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
import android.view.accessibility.AccessibilityManager
@@ -54,9 +55,13 @@ import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
import com.android.systemui.biometrics.ui.viewmodel.PromptSize
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.VibratorHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -77,6 +82,8 @@ object BiometricViewBinder {
backgroundView: View,
legacyCallback: Callback,
applicationScope: CoroutineScope,
vibratorHelper: VibratorHelper,
featureFlags: FeatureFlags,
): AuthBiometricViewAdapter {
val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
@@ -383,6 +390,18 @@ object BiometricViewBinder {
}
}
}
// Play haptics
if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
launch {
viewModel.hapticsToPlay.collect { hapticFeedbackConstant ->
if (hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) {
vibratorHelper.performHapticFeedback(view, hapticFeedbackConstant)
viewModel.clearHaptics()
}
}
}
}
}
}

View File

@@ -17,11 +17,14 @@ package com.android.systemui.biometrics.ui.viewmodel
import android.hardware.biometrics.BiometricPrompt
import android.util.Log
import android.view.HapticFeedbackConstants
import com.android.systemui.biometrics.AuthBiometricView
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
import com.android.systemui.biometrics.domain.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.statusbar.VibratorHelper
import javax.inject.Inject
import kotlinx.coroutines.Job
@@ -43,6 +46,7 @@ class PromptViewModel
constructor(
private val interactor: PromptSelectorInteractor,
private val vibrator: VibratorHelper,
private val featureFlags: FeatureFlags,
) {
/** The set of modalities available for this prompt */
val modalities: Flow<BiometricModalities> =
@@ -90,6 +94,11 @@ constructor(
private val _forceLargeSize = MutableStateFlow(false)
private val _forceMediumSize = MutableStateFlow(false)
private val _hapticsToPlay = MutableStateFlow(HapticFeedbackConstants.NO_HAPTICS)
/** Event fired to the view indicating a [HapticFeedbackConstants] to be played */
val hapticsToPlay = _hapticsToPlay.asStateFlow()
/** The size of the prompt. */
val size: Flow<PromptSize> =
combine(
@@ -438,11 +447,26 @@ constructor(
_forceLargeSize.value = true
}
private fun VibratorHelper.success(modality: BiometricModality) =
vibrateAuthSuccess("$TAG, modality = $modality BP::success")
private fun VibratorHelper.success(modality: BiometricModality) {
if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
_hapticsToPlay.value = HapticFeedbackConstants.CONFIRM
} else {
vibrateAuthSuccess("$TAG, modality = $modality BP::success")
}
}
private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) =
vibrateAuthError("$TAG, modality = $modality BP::error")
private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) {
if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
_hapticsToPlay.value = HapticFeedbackConstants.REJECT
} else {
vibrateAuthError("$TAG, modality = $modality BP::error")
}
}
/** Clears the [hapticsToPlay] variable by setting it to the NO_HAPTICS default. */
fun clearHaptics() {
_hapticsToPlay.value = HapticFeedbackConstants.NO_HAPTICS
}
companion object {
private const val TAG = "PromptViewModel"

View File

@@ -139,6 +139,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
@Before
fun setup() {
featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt)
featureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false)
}
@After
@@ -151,7 +152,10 @@ open class AuthContainerViewTest : SysuiTestCase() {
@Test
fun testNotifiesAnimatedIn() {
initializeFingerprintContainer()
verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
verify(callback).onDialogAnimatedIn(
authContainer?.requestId ?: 0L,
true /* startFingerprintNow */
)
}
@Test
@@ -196,7 +200,10 @@ open class AuthContainerViewTest : SysuiTestCase() {
waitForIdleSync()
// attaching the view resets the state and allows this to happen again
verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
verify(callback).onDialogAnimatedIn(
authContainer?.requestId ?: 0L,
true /* startFingerprintNow */
)
}
@Test
@@ -211,7 +218,10 @@ open class AuthContainerViewTest : SysuiTestCase() {
// the first time is triggered by initializeFingerprintContainer()
// the second time was triggered by dismissWithoutCallback()
verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
verify(callback, times(2)).onDialogAnimatedIn(
authContainer?.requestId ?: 0L,
true /* startFingerprintNow */
)
}
@Test
@@ -517,10 +527,11 @@ open class AuthContainerViewTest : SysuiTestCase() {
{ authBiometricFingerprintViewModel },
{ promptSelectorInteractor },
{ bpCredentialInteractor },
PromptViewModel(promptSelectorInteractor, vibrator),
PromptViewModel(promptSelectorInteractor, vibrator, featureFlags),
{ credentialViewModel },
Handler(TestableLooper.get(this).looper),
fakeExecutor
fakeExecutor,
vibrator
) {
override fun postOnAnimation(runnable: Runnable) {
runnable.run()

View File

@@ -197,6 +197,8 @@ public class AuthControllerTest extends SysuiTestCase {
private ArgumentCaptor<String> mMessageCaptor;
@Mock
private Resources mResources;
@Mock
private VibratorHelper mVibratorHelper;
private TestableContext mContextSpy;
private Execution mExecution;
@@ -1097,7 +1099,7 @@ public class AuthControllerTest extends SysuiTestCase {
() -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
() -> mCredentialViewModel, () -> mPromptViewModel,
mInteractionJankMonitor, mHandler,
mBackgroundExecutor, mUdfpsUtils);
mBackgroundExecutor, mUdfpsUtils, mVibratorHelper);
}
@Override

View File

@@ -19,6 +19,7 @@ package com.android.systemui.biometrics.ui.viewmodel
import android.hardware.biometrics.PromptInfo
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.view.HapticFeedbackConstants
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
@@ -33,6 +34,8 @@ 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.collectValues
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
@@ -71,13 +74,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
private lateinit var selector: PromptSelectorInteractor
private lateinit var viewModel: PromptViewModel
private val featureFlags = FakeFeatureFlags()
@Before
fun setup() {
selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
selector.resetPrompt()
viewModel = PromptViewModel(selector, vibrator)
viewModel = PromptViewModel(selector, vibrator, featureFlags)
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
}
@Test
@@ -149,6 +154,29 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
verify(vibrator, never()).vibrateAuthError(any())
}
@Test
fun playSuccessHaptic_onwayHapticsEnabled_SetsConfirmConstant() = runGenericTest {
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
viewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
if (expectConfirmation) {
viewModel.confirmAuthenticated()
}
val currentConstant by collectLastValue(viewModel.hapticsToPlay)
assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.CONFIRM)
}
@Test
fun playErrorHaptic_onwayHapticsEnabled_SetsRejectConstant() = runGenericTest {
featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
viewModel.showTemporaryError("test", "messageAfterError", false)
val currentConstant by collectLastValue(viewModel.hapticsToPlay)
assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.REJECT)
}
private suspend fun TestScope.showAuthenticated(
authenticatedModality: BiometricModality,
expectConfirmation: Boolean,