diff options
7 files changed, 117 insertions, 20 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 58adfa1d882c..58c8000a2328 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -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()) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 3df7ca58736a..60e4cd02456b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 75b26f859809..490edc68b913 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -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() + } + } + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index dca19c503fd3..655e74ae262b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -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) { + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + _hapticsToPlay.value = HapticFeedbackConstants.REJECT + } else { + vibrateAuthError("$TAG, modality = $modality BP::error") + } + } - private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) = - 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" diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index e3e61306bcd7..4e52e64a8af1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -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() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 3d4171ff9cf3..bf2020bb3d37 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 40b1f207894a..7e6b74a8dce6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -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, |