Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 96e79d56 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

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
parent 8a9733fc
Loading
Loading
Loading
Loading
+14 −6
Original line number Diff line number Diff line
@@ -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()) {
+8 −3
Original line number Diff line number Diff line
@@ -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
+19 −0
Original line number Diff line number Diff line
@@ -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()
                            }
                        }
                    }
                }
            }
        }

+28 −4
Original line number Diff line number Diff line
@@ -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) =
    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) =
    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"
+16 −5
Original line number Diff line number Diff line
@@ -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()
Loading