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:
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user