Merge "Adding MSDL feedback to the Biometric prompt." into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index b7d99d2..65825b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -97,6 +97,8 @@
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
+import com.google.android.msdl.domain.MSDLPlayer;
+
 import dagger.Lazy;
 
 import org.junit.Before;
@@ -185,6 +187,8 @@
     private Resources mResources;
     @Mock
     private VibratorHelper mVibratorHelper;
+    @Mock
+    private MSDLPlayer mMSDLPlayer;
 
     private TestableContext mContextSpy;
     private Execution mExecution;
@@ -1066,7 +1070,7 @@
                     () -> mLogContextInteractor, () -> mPromptSelectionInteractor,
                     () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor,
                     mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper,
-                    mLazyViewCapture);
+                    mLazyViewCapture, mMSDLPlayer);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 970fdea..69ab976 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -78,6 +78,8 @@
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
+import com.google.android.msdl.domain.MSDLPlayer;
+
 import kotlin.Lazy;
 
 import kotlinx.coroutines.CoroutineScope;
@@ -157,6 +159,8 @@
 
     private final @Background DelayableExecutor mBackgroundExecutor;
 
+    private final MSDLPlayer mMSDLPlayer;
+
     // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet.
     @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason;
     // HAT received from LockSettingsService when credential is verified.
@@ -292,7 +296,8 @@
             @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
             @NonNull @Background DelayableExecutor bgExecutor,
             @NonNull VibratorHelper vibratorHelper,
-            Lazy<ViewCapture> lazyViewCapture) {
+            Lazy<ViewCapture> lazyViewCapture,
+            @NonNull MSDLPlayer msdlPlayer) {
         super(config.mContext);
 
         mConfig = config;
@@ -309,6 +314,7 @@
                 .getDimension(R.dimen.biometric_dialog_animation_translation_offset);
         mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
         mBiometricCallback = new BiometricCallback();
+        mMSDLPlayer = msdlPlayer;
 
         final BiometricModalities biometricModalities = new BiometricModalities(
                 Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
@@ -379,7 +385,7 @@
                 getJankListener(mLayout, TRANSIT,
                         BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
                 mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
-                vibratorHelper);
+                vibratorHelper, mMSDLPlayer);
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 097ab72..b39aae9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -89,6 +89,8 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.Execution;
 
+import com.google.android.msdl.domain.MSDLPlayer;
+
 import dagger.Lazy;
 
 import kotlin.Unit;
@@ -183,6 +185,7 @@
     private final @Background DelayableExecutor mBackgroundExecutor;
     private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
     @NonNull private final VibratorHelper mVibratorHelper;
+    @NonNull private final MSDLPlayer mMSDLPlayer;
 
     private final kotlin.Lazy<ViewCapture> mLazyViewCapture;
 
@@ -742,7 +745,8 @@
             @Background DelayableExecutor bgExecutor,
             @NonNull UdfpsUtils udfpsUtils,
             @NonNull VibratorHelper vibratorHelper,
-            Lazy<ViewCapture> daggerLazyViewCapture) {
+            Lazy<ViewCapture> daggerLazyViewCapture,
+            @NonNull MSDLPlayer msdlPlayer) {
         mContext = context;
         mExecution = execution;
         mUserManager = userManager;
@@ -764,6 +768,7 @@
         mUdfpsUtils = udfpsUtils;
         mApplicationCoroutineScope = applicationCoroutineScope;
         mVibratorHelper = vibratorHelper;
+        mMSDLPlayer = msdlPlayer;
 
         mLogContextInteractor = logContextInteractor;
         mPromptSelectorInteractor = promptSelectorInteractorProvider;
@@ -1327,7 +1332,7 @@
                 wakefulnessLifecycle, userManager, lockPatternUtils,
                 mInteractionJankMonitor, mPromptSelectorInteractor, viewModel,
                 mCredentialViewModelProvider, bgExecutor, mVibratorHelper,
-                mLazyViewCapture);
+                mLazyViewCapture, mMSDLPlayer);
     }
 
     @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 0b440ad..e7e8d8f 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,7 +25,6 @@
 import android.hardware.biometrics.Flags
 import android.hardware.face.FaceManager
 import android.util.Log
-import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
 import android.view.View
 import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
@@ -59,6 +58,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
+import com.google.android.msdl.domain.MSDLPlayer
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.combine
@@ -83,6 +83,7 @@
         legacyCallback: Spaghetti.Callback,
         applicationScope: CoroutineScope,
         vibratorHelper: VibratorHelper,
+        msdlPlayer: MSDLPlayer,
     ): Spaghetti {
         val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
 
@@ -434,21 +435,27 @@
                 // Play haptics
                 launch {
                     viewModel.hapticsToPlay.collect { haptics ->
-                        if (haptics.hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) {
-                            if (haptics.flag != null) {
-                                vibratorHelper.performHapticFeedback(
-                                    view,
-                                    haptics.hapticFeedbackConstant,
-                                    haptics.flag,
-                                )
-                            } else {
-                                vibratorHelper.performHapticFeedback(
-                                    view,
-                                    haptics.hapticFeedbackConstant,
-                                )
+                        when (haptics) {
+                            is PromptViewModel.HapticsToPlay.HapticConstant -> {
+                                if (haptics.flag != null) {
+                                    vibratorHelper.performHapticFeedback(
+                                        view,
+                                        haptics.constant,
+                                        haptics.flag,
+                                    )
+                                } else {
+                                    vibratorHelper.performHapticFeedback(
+                                        view,
+                                        haptics.constant,
+                                    )
+                                }
                             }
-                            viewModel.clearHaptics()
+                            is PromptViewModel.HapticsToPlay.MSDL -> {
+                                msdlPlayer.playToken(haptics.token, haptics.properties)
+                            }
+                            is PromptViewModel.HapticsToPlay.None -> {}
                         }
+                        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 4c2fe07..168ba11 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
@@ -35,7 +35,9 @@
 import android.util.RotationUtils
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
+import com.android.keyguard.AuthInteractionProperties
 import com.android.launcher3.icons.IconProvider
+import com.android.systemui.Flags.msdlFeedback
 import com.android.systemui.biometrics.UdfpsUtils
 import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.Utils.isSystem
@@ -53,6 +55,8 @@
 import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
 import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.combine
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.InteractionProperties
 import javax.inject.Inject
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
@@ -245,8 +249,9 @@
     private val _forceLargeSize = MutableStateFlow(false)
     private val _forceMediumSize = MutableStateFlow(false)
 
-    private val _hapticsToPlay =
-        MutableStateFlow(HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, /* flag= */ null))
+    private val authInteractionProperties = AuthInteractionProperties()
+    private val _hapticsToPlay: MutableStateFlow<HapticsToPlay> =
+        MutableStateFlow(HapticsToPlay.None)
 
     /** Event fired to the view indicating a [HapticsToPlay] */
     val hapticsToPlay = _hapticsToPlay.asStateFlow()
@@ -939,26 +944,52 @@
     }
 
     private fun vibrateOnSuccess() {
-        _hapticsToPlay.value =
-            HapticsToPlay(
-                HapticFeedbackConstants.BIOMETRIC_CONFIRM,
-                null,
-            )
+        val haptics =
+            if (msdlFeedback()) {
+                HapticsToPlay.MSDL(MSDLToken.UNLOCK, authInteractionProperties)
+            } else {
+                HapticsToPlay.HapticConstant(
+                    HapticFeedbackConstants.BIOMETRIC_CONFIRM,
+                    flag = null,
+                )
+            }
+        _hapticsToPlay.value = haptics
     }
 
     private fun vibrateOnError() {
-        _hapticsToPlay.value =
-            HapticsToPlay(
-                HapticFeedbackConstants.BIOMETRIC_REJECT,
-                null,
-            )
+        val haptics =
+            if (msdlFeedback()) {
+                HapticsToPlay.MSDL(MSDLToken.FAILURE, authInteractionProperties)
+            } else {
+                HapticsToPlay.HapticConstant(
+                    HapticFeedbackConstants.BIOMETRIC_REJECT,
+                    flag = null,
+                )
+            }
+        _hapticsToPlay.value = haptics
     }
 
     /** Clears the [hapticsToPlay] variable by setting its constant to the NO_HAPTICS default. */
     fun clearHaptics() {
-        _hapticsToPlay.update { previous ->
-            HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, previous.flag)
-        }
+        _hapticsToPlay.update { HapticsToPlay.None }
+    }
+
+    /** The state of haptic feedback to play. */
+    sealed interface HapticsToPlay {
+        /**
+         * Haptics using [HapticFeedbackConstants]. It is composed by a [HapticFeedbackConstants]
+         * and a [HapticFeedbackConstants] flag.
+         */
+        data class HapticConstant(val constant: Int, val flag: Int?) : HapticsToPlay
+
+        /**
+         * Haptics using MSDL feedback. It is composed by a [MSDLToken] and optional
+         * [InteractionProperties]
+         */
+        data class MSDL(val token: MSDLToken, val properties: InteractionProperties?) :
+            HapticsToPlay
+
+        data object None : HapticsToPlay
     }
 
     companion object {
@@ -1095,9 +1126,3 @@
     val isStarted: Boolean
         get() = this == Normal || this == Delayed
 }
-
-/**
- * The state of haptic feedback to play. It is composed by a [HapticFeedbackConstants] and a
- * [HapticFeedbackConstants] flag.
- */
-data class HapticsToPlay(val hapticFeedbackConstant: Int, val flag: Int?)
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 1e23690..7889b3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -61,10 +61,12 @@
 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
 import com.android.systemui.display.data.repository.FakeDisplayRepository
+import com.android.systemui.haptics.msdl.msdlPlayer
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.events.ANIMATING_OUT
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
@@ -145,6 +147,9 @@
     private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor)
     private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android)
 
+    private val kosmos = testKosmos()
+    private val msdlPlayer = kosmos.msdlPlayer
+
     private var authContainer: TestAuthContainerView? = null
 
     @Before
@@ -668,7 +673,8 @@
             { credentialViewModel },
             fakeExecutor,
             vibrator,
-            lazyViewCapture
+            lazyViewCapture,
+            msdlPlayer,
         ) {
         override fun postOnAnimation(runnable: Runnable) {
             runnable.run()
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 4fc4166..55fd344 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
@@ -37,12 +37,15 @@
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
 import android.view.Surface
 import androidx.test.filters.SmallTest
 import com.android.app.activityTaskManager
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.biometrics.Utils.toBitmap
@@ -72,6 +75,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.util.mockito.withArgCaptor
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.first
@@ -124,6 +128,7 @@
     private val defaultLogoDescriptionFromActivityInfo = "Test Coke App"
     private val logoDescriptionFromApp = "Test Cake App"
     private val packageNameForLogoWithOverrides = "should.use.overridden.logo"
+    private val authInteractionProperties = AuthInteractionProperties()
 
     /** Prompt panel size padding */
     private val smallHorizontalGuidelinePadding =
@@ -707,31 +712,66 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun set_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() =
         runGenericTest {
             val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
 
             kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
 
-            val confirmHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
-            assertThat(confirmHaptics?.hapticFeedbackConstant)
-                .isEqualTo(
-                    if (expectConfirmation) HapticFeedbackConstants.NO_HAPTICS
-                    else HapticFeedbackConstants.BIOMETRIC_CONFIRM
-                )
-            assertThat(confirmHaptics?.flag).isNull()
+            val hapticsPreConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+            if (expectConfirmation) {
+                assertThat(hapticsPreConfirm).isEqualTo(PromptViewModel.HapticsToPlay.None)
+            } else {
+                val confirmHaptics =
+                    hapticsPreConfirm as PromptViewModel.HapticsToPlay.HapticConstant
+                assertThat(confirmHaptics.constant)
+                    .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
+                assertThat(confirmHaptics.flag).isNull()
+            }
 
             if (expectConfirmation) {
                 kosmos.promptViewModel.confirmAuthenticated()
             }
 
-            val confirmedHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
-            assertThat(confirmedHaptics?.hapticFeedbackConstant)
+            val hapticsPostConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+            val confirmedHaptics =
+                hapticsPostConfirm as PromptViewModel.HapticsToPlay.HapticConstant
+            assertThat(confirmedHaptics.constant)
                 .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
-            assertThat(confirmedHaptics?.flag).isNull()
+            assertThat(confirmedHaptics.flag).isNull()
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun set_msdl_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() =
+        runGenericTest {
+            val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+
+            kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
+
+            val hapticsPreConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+
+            if (expectConfirmation) {
+                assertThat(hapticsPreConfirm).isEqualTo(PromptViewModel.HapticsToPlay.None)
+            } else {
+                val confirmHaptics = hapticsPreConfirm as PromptViewModel.HapticsToPlay.MSDL
+                assertThat(confirmHaptics.token).isEqualTo(MSDLToken.UNLOCK)
+                assertThat(confirmHaptics.properties).isEqualTo(authInteractionProperties)
+            }
+
+            if (expectConfirmation) {
+                kosmos.promptViewModel.confirmAuthenticated()
+            }
+
+            val hapticsPostConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+            val confirmedHaptics = hapticsPostConfirm as PromptViewModel.HapticsToPlay.MSDL
+            assertThat(confirmedHaptics.token).isEqualTo(MSDLToken.UNLOCK)
+            assertThat(confirmedHaptics.properties).isEqualTo(authInteractionProperties)
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun playSuccessHaptic_SetsConfirmConstant() = runGenericTest {
         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
@@ -740,20 +780,48 @@
             kosmos.promptViewModel.confirmAuthenticated()
         }
 
-        val currentHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
-        assertThat(currentHaptics?.hapticFeedbackConstant)
-            .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
-        assertThat(currentHaptics?.flag).isNull()
+        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val currentHaptics = haptics as PromptViewModel.HapticsToPlay.HapticConstant
+        assertThat(currentHaptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
+        assertThat(currentHaptics.flag).isNull()
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun playSuccessHaptic_SetsUnlockMSDLFeedback() = runGenericTest {
+        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+        kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L)
+
+        if (expectConfirmation) {
+            kosmos.promptViewModel.confirmAuthenticated()
+        }
+
+        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val currentHaptics = haptics as PromptViewModel.HapticsToPlay.MSDL
+        assertThat(currentHaptics.token).isEqualTo(MSDLToken.UNLOCK)
+        assertThat(currentHaptics.properties).isEqualTo(authInteractionProperties)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun playErrorHaptic_SetsRejectConstant() = runGenericTest {
         kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false)
 
-        val currentHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
-        assertThat(currentHaptics?.hapticFeedbackConstant)
-            .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
-        assertThat(currentHaptics?.flag).isNull()
+        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val currentHaptics = haptics as PromptViewModel.HapticsToPlay.HapticConstant
+        assertThat(currentHaptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
+        assertThat(currentHaptics.flag).isNull()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun playErrorHaptic_SetsFailureMSDLFeedback() = runGenericTest {
+        kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false)
+
+        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val currentHaptics = haptics as PromptViewModel.HapticsToPlay.MSDL
+        assertThat(currentHaptics.token).isEqualTo(MSDLToken.FAILURE)
+        assertThat(currentHaptics.properties).isEqualTo(authInteractionProperties)
     }
 
     // biometricprompt_sfps_fingerprint_authenticating reused across rotations
@@ -855,6 +923,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun set_haptic_on_errors() = runGenericTest {
         kosmos.promptViewModel.showTemporaryError(
             "so sad",
@@ -863,13 +932,30 @@
             hapticFeedback = true,
         )
 
-        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
-        assertThat(haptics?.hapticFeedbackConstant)
-            .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
-        assertThat(haptics?.flag).isNull()
+        val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.HapticConstant
+        assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
+        assertThat(haptics.flag).isNull()
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun set_msdl_haptic_on_errors() = runGenericTest {
+        kosmos.promptViewModel.showTemporaryError(
+            "so sad",
+            messageAfterError = "",
+            authenticateAfterError = false,
+            hapticFeedback = true,
+        )
+
+        val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.MSDL
+        assertThat(haptics.token).isEqualTo(MSDLToken.FAILURE)
+        assertThat(haptics.properties).isEqualTo(authInteractionProperties)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun plays_haptic_on_errors_unless_skipped() = runGenericTest {
         kosmos.promptViewModel.showTemporaryError(
             "still sad",
@@ -878,11 +964,26 @@
             hapticFeedback = false,
         )
 
-        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
-        assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.NO_HAPTICS)
+        val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        assertThat(hapticsToPlay).isEqualTo(PromptViewModel.HapticsToPlay.None)
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun plays_msdl_haptic_on_errors_unless_skipped() = runGenericTest {
+        kosmos.promptViewModel.showTemporaryError(
+            "still sad",
+            messageAfterError = "",
+            authenticateAfterError = false,
+            hapticFeedback = false,
+        )
+
+        val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        assertThat(hapticsToPlay).isEqualTo(PromptViewModel.HapticsToPlay.None)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
     fun plays_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest {
         val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
         kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
@@ -894,17 +995,39 @@
             hapticFeedback = true,
         )
 
-        val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.HapticConstant
         if (expectConfirmation) {
-            assertThat(haptics?.hapticFeedbackConstant)
-                .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
-            assertThat(haptics?.flag).isNull()
+            assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT)
+            assertThat(haptics.flag).isNull()
         } else {
-            assertThat(haptics?.hapticFeedbackConstant)
-                .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
+            assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM)
         }
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun plays_msdl_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest {
+        val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+        kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+        kosmos.promptViewModel.showTemporaryError(
+            "still sad",
+            messageAfterError = "",
+            authenticateAfterError = false,
+            hapticFeedback = true,
+        )
+
+        val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay)
+        val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.MSDL
+        if (expectConfirmation) {
+            assertThat(haptics.token).isEqualTo(MSDLToken.FAILURE)
+        } else {
+            assertThat(haptics.token).isEqualTo(MSDLToken.UNLOCK)
+        }
+        assertThat(haptics.properties).isEqualTo(authInteractionProperties)
+    }
+
     private suspend fun TestScope.showTemporaryErrors(
         restart: Boolean,
         helpAfterError: String = "",