Merge "Adding MSDL feedback to Compose Bouncers." into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 163b355..8321238 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.bouncer.ui.composable
 
-import android.view.HapticFeedbackConstants
 import androidx.annotation.VisibleForTesting
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationVector1D
@@ -133,10 +132,7 @@
         // Perform haptic feedback, but only if the current dot is not null, so we don't perform it
         // when the UI first shows up or when the user lifts their pointer/finger.
         if (currentDot != null) {
-            view.performHapticFeedback(
-                HapticFeedbackConstants.VIRTUAL_KEY,
-                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
-            )
+            viewModel.performDotFeedback(view)
         }
 
         if (!isAnimationEnabled) {
@@ -206,10 +202,7 @@
     // Show the failure animation if the user entered the wrong input.
     LaunchedEffect(animateFailure) {
         if (animateFailure) {
-            showFailureAnimation(
-                dots = dots,
-                scalingAnimatables = dotScalingAnimatables,
-            )
+            showFailureAnimation(dots = dots, scalingAnimatables = dotScalingAnimatables)
             viewModel.onFailureAnimationShown()
         }
     }
@@ -358,15 +351,10 @@
                     (1 - checkNotNull(dotAppearMoveUpAnimatables[dot]).value) * initialOffset
                 drawCircle(
                     center =
-                        pixelOffset(
-                            dot,
-                            spacing,
-                            horizontalOffset,
-                            verticalOffset + appearOffset,
-                        ),
+                        pixelOffset(dot, spacing, horizontalOffset, verticalOffset + appearOffset),
                     color =
                         dotColor.copy(alpha = checkNotNull(dotAppearFadeInAnimatables[dot]).value),
-                    radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value
+                    radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value,
                 )
             }
         }
@@ -387,7 +375,7 @@
                             delayMillis = 33 * dot.y,
                             durationMillis = 450,
                             easing = Easings.LegacyDecelerate,
-                        )
+                        ),
                 )
             }
         }
@@ -400,7 +388,7 @@
                             delayMillis = 0,
                             durationMillis = 450 + (33 * dot.y),
                             easing = Easings.StandardDecelerate,
-                        )
+                        ),
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 489e24e..0830c9b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.bouncer.ui.composable
 
-import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
+import android.view.View
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
@@ -72,11 +72,7 @@
 
 /** Renders the PIN button pad. */
 @Composable
-fun PinPad(
-    viewModel: PinBouncerViewModel,
-    verticalSpacing: Dp,
-    modifier: Modifier = Modifier,
-) {
+fun PinPad(viewModel: PinBouncerViewModel, verticalSpacing: Dp, modifier: Modifier = Modifier) {
     DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsStateWithLifecycle()
@@ -104,7 +100,7 @@
         columns = columns,
         verticalSpacing = verticalSpacing,
         horizontalSpacing = calculateHorizontalSpacingBetweenColumns(gridWidth = 300.dp),
-        modifier = modifier.focusRequester(focusRequester).sysuiResTag("pin_pad_grid")
+        modifier = modifier.focusRequester(focusRequester).sysuiResTag("pin_pad_grid"),
     ) {
         repeat(9) { index ->
             DigitButton(
@@ -126,10 +122,11 @@
                 ),
             isInputEnabled = isInputEnabled,
             onClicked = viewModel::onBackspaceButtonClicked,
+            onPointerDown = viewModel::onBackspaceButtonPressed,
             onLongPressed = viewModel::onBackspaceButtonLongPressed,
             appearance = backspaceButtonAppearance,
             scaling = buttonScaleAnimatables[9]::value,
-            elementId = "delete_button"
+            elementId = "delete_button",
         )
 
         DigitButton(
@@ -138,7 +135,7 @@
             onClicked = viewModel::onPinButtonClicked,
             scaling = buttonScaleAnimatables[10]::value,
             isAnimationEnabled = isDigitButtonAnimationEnabled,
-            onPointerDown = viewModel::onDigitButtonDown
+            onPointerDown = viewModel::onDigitButtonDown,
         )
 
         ActionButton(
@@ -152,7 +149,7 @@
             onClicked = viewModel::onAuthenticateButtonClicked,
             appearance = confirmButtonAppearance,
             scaling = buttonScaleAnimatables[11]::value,
-            elementId = "key_enter"
+            elementId = "key_enter",
         )
     }
 }
@@ -162,7 +159,7 @@
     digit: Int,
     isInputEnabled: Boolean,
     onClicked: (Int) -> Unit,
-    onPointerDown: () -> Unit,
+    onPointerDown: (View?) -> Unit,
     scaling: () -> Float,
     isAnimationEnabled: Boolean,
 ) {
@@ -178,7 +175,7 @@
                 val scale = if (isAnimationEnabled) scaling() else 1f
                 scaleX = scale
                 scaleY = scale
-            }
+            },
     ) { contentColor ->
         // TODO(b/281878426): once "color: () -> Color" (added to BasicText in aosp/2568972) makes
         // it into Text, use that here, to animate more efficiently.
@@ -197,6 +194,7 @@
     onClicked: () -> Unit,
     elementId: String,
     onLongPressed: (() -> Unit)? = null,
+    onPointerDown: ((View?) -> Unit)? = null,
     appearance: ActionButtonAppearance,
     scaling: () -> Float,
 ) {
@@ -222,18 +220,16 @@
         foregroundColor = foregroundColor,
         isAnimationEnabled = true,
         elementId = elementId,
+        onPointerDown = onPointerDown,
         modifier =
             Modifier.graphicsLayer {
                 alpha = hiddenAlpha
                 val scale = scaling()
                 scaleX = scale
                 scaleY = scale
-            }
+            },
     ) { contentColor ->
-        Icon(
-            icon = icon,
-            tint = contentColor(),
-        )
+        Icon(icon = icon, tint = contentColor())
     }
 }
 
@@ -247,22 +243,13 @@
     modifier: Modifier = Modifier,
     elementId: String? = null,
     onLongPressed: (() -> Unit)? = null,
-    onPointerDown: (() -> Unit)? = null,
+    onPointerDown: ((View?) -> Unit)? = null,
     content: @Composable (contentColor: () -> Color) -> Unit,
 ) {
     val interactionSource = remember { MutableInteractionSource() }
     val isPressed by interactionSource.collectIsPressedAsState()
     val indication = LocalIndication.current.takeUnless { isPressed }
-
     val view = LocalView.current
-    LaunchedEffect(isPressed) {
-        if (isPressed) {
-            view.performHapticFeedback(
-                HapticFeedbackConstants.VIRTUAL_KEY,
-                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
-            )
-        }
-    }
 
     // Pin button animation specification is asymmetric: fast animation to the pressed state, and a
     // slow animation upon release. Note that isPressed is guaranteed to be true for at least the
@@ -277,7 +264,7 @@
         animateDpAsState(
             if (isAnimationEnabled && isPressed) 24.dp else pinButtonMaxSize / 2,
             label = "PinButton round corners",
-            animationSpec = tween(animDurationMillis, easing = animEasing)
+            animationSpec = tween(animDurationMillis, easing = animEasing),
         )
     val colorAnimationSpec: AnimationSpec<Color> = tween(animDurationMillis, easing = animEasing)
     val containerColor: Color by
@@ -287,7 +274,7 @@
                 else -> backgroundColor
             },
             label = "Pin button container color",
-            animationSpec = colorAnimationSpec
+            animationSpec = colorAnimationSpec,
         )
     val contentColor =
         animateColorAsState(
@@ -296,7 +283,7 @@
                 else -> foregroundColor
             },
             label = "Pin button container color",
-            animationSpec = colorAnimationSpec
+            animationSpec = colorAnimationSpec,
         )
 
     Box(
@@ -319,11 +306,11 @@
                             interactionSource = interactionSource,
                             indication = indication,
                             onClick = onClicked,
-                            onLongClick = onLongPressed
+                            onLongClick = onLongPressed,
                         )
                         .pointerInteropFilter { motionEvent ->
                             if (motionEvent.action == MotionEvent.ACTION_DOWN) {
-                                onPointerDown?.let { it() }
+                                onPointerDown?.let { it(view) }
                             }
                             false
                         }
@@ -353,10 +340,7 @@
                 animatable.animateTo(
                     targetValue = 1f,
                     animationSpec =
-                        tween(
-                            durationMillis = pinButtonErrorRevertMs,
-                            easing = Easings.Legacy,
-                        ),
+                        tween(durationMillis = pinButtonErrorRevertMs, easing = Easings.Legacy),
                 )
             }
         }
@@ -364,9 +348,7 @@
 }
 
 /** Returns the amount of horizontal spacing between columns, in dips. */
-private fun calculateHorizontalSpacingBetweenColumns(
-    gridWidth: Dp,
-): Dp {
+private fun calculateHorizontalSpacingBetweenColumns(gridWidth: Dp): Dp {
     return (gridWidth - (pinButtonMaxSize * columns)) / (columns - 1)
 }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index deef652..9552564 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -16,18 +16,26 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -39,11 +47,15 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
+    private val msdlPlayer = kosmos.fakeMSDLPlayer
+    private val bouncerHapticPlayer = kosmos.bouncerHapticPlayer
+    private val authInteractionProperties = AuthInteractionProperties()
     private val underTest =
         kosmos.pinBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true),
             onIntentionalUserInput = {},
             authenticationMethod = AuthenticationMethodModel.Pin,
+            bouncerHapticPlayer = bouncerHapticPlayer,
         )
 
     @Before
@@ -77,4 +89,42 @@
             underTest.onAuthenticateButtonClicked()
             assertThat(animateFailure).isFalse()
         }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onAuthenticationResult_playUnlockTokenIfSuccessful() =
+        testScope.runTest {
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            // Correct PIN:
+            FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
+                underTest.onPinButtonClicked(digit)
+            }
+            underTest.onAuthenticateButtonClicked()
+            runCurrent()
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
+        }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onAuthenticationResult_playFailureTokenIfFailure() =
+        testScope.runTest {
+            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+                AuthenticationMethodModel.Pin
+            )
+            // Wrong PIN:
+            FakeAuthenticationRepository.DEFAULT_PIN.drop(2).forEach { digit ->
+                underTest.onPinButtonClicked(digit)
+            }
+            underTest.onAuthenticateButtonClicked()
+            runCurrent()
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 7c773a9..c163c6f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -16,9 +16,11 @@
 
 package com.android.systemui.bouncer.ui.viewmodel
 
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.authenticationRepository
@@ -27,12 +29,16 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.msdl.FakeMSDLPlayer
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -55,10 +61,13 @@
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val bouncerViewModel by lazy { kosmos.bouncerSceneContentViewModel }
+    private val msdlPlayer: FakeMSDLPlayer = kosmos.fakeMSDLPlayer
+    private val bouncerHapticHelper = kosmos.bouncerHapticPlayer
     private val underTest =
         kosmos.patternBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
             onIntentionalUserInput = {},
+            bouncerHapticPlayer = bouncerHapticHelper,
         )
 
     private val containerSize = 90 // px
@@ -115,10 +124,7 @@
                     .that(selectedDots)
                     .isEqualTo(
                         CORRECT_PATTERN.subList(0, index + 1).map {
-                            PatternDotViewModel(
-                                x = it.x,
-                                y = it.y,
-                            )
+                            PatternDotViewModel(x = it.x, y = it.y)
                         }
                     )
                 assertWithMessage("Wrong current dot for index $index")
@@ -174,7 +180,7 @@
                     listOf(
                         PatternDotViewModel(0, 0),
                         PatternDotViewModel(1, 0),
-                        PatternDotViewModel(2, 0)
+                        PatternDotViewModel(2, 0),
                     )
                 )
         }
@@ -200,7 +206,7 @@
                     listOf(
                         PatternDotViewModel(1, 0),
                         PatternDotViewModel(1, 1),
-                        PatternDotViewModel(1, 2)
+                        PatternDotViewModel(1, 2),
                     )
                 )
         }
@@ -228,7 +234,7 @@
                     listOf(
                         PatternDotViewModel(2, 0),
                         PatternDotViewModel(1, 1),
-                        PatternDotViewModel(0, 2)
+                        PatternDotViewModel(0, 2),
                     )
                 )
         }
@@ -300,10 +306,7 @@
             val attempts = FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT + 1
             repeat(attempts) { attempt ->
                 underTest.onDragStart()
-                CORRECT_PATTERN.subList(
-                        0,
-                        kosmos.authenticationRepository.minPatternLength - 1,
-                    )
+                CORRECT_PATTERN.subList(0, kosmos.authenticationRepository.minPatternLength - 1)
                     .forEach { coordinate ->
                         underTest.onDrag(
                             xPx = 30f * coordinate.x + 15,
@@ -341,6 +344,16 @@
             assertThat(authResult).isTrue()
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun performDotFeedback_deliversDragToken() =
+        testScope.runTest {
+            underTest.performDotFeedback(null)
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
     private fun dragOverCoordinates(vararg coordinatesDragged: Point) {
         underTest.onDragStart()
         coordinatesDragged.forEach(::dragToCoordinate)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 2ee4aee..af5f2ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -27,6 +27,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
@@ -35,12 +36,15 @@
 import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository
 import com.android.systemui.classifier.fakeFalsingCollector
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
+import com.android.systemui.haptics.msdl.fakeMSDLPlayer
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import kotlin.random.Random
 import kotlin.random.nextInt
@@ -64,11 +68,14 @@
     private val testScope = kosmos.testScope
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+    private val msdlPlayer = kosmos.fakeMSDLPlayer
+    private val bouncerHapticPlayer = kosmos.bouncerHapticPlayer
     private val underTest by lazy {
         kosmos.pinBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true),
             onIntentionalUserInput = {},
             authenticationMethod = AuthenticationMethodModel.Pin,
+            bouncerHapticPlayer = bouncerHapticPlayer,
         )
     }
 
@@ -97,6 +104,7 @@
                     isInputEnabled = MutableStateFlow(true),
                     onIntentionalUserInput = {},
                     authenticationMethod = AuthenticationMethodModel.Sim,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
 
             assertThat(underTest.isSimAreaVisible).isTrue()
@@ -122,6 +130,7 @@
                     isInputEnabled = MutableStateFlow(true),
                     onIntentionalUserInput = {},
                     authenticationMethod = AuthenticationMethodModel.Pin,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
             val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -487,11 +496,39 @@
         testScope.runTest {
             lockDeviceAndOpenPinBouncer()
 
-            underTest.onDigitButtonDown()
+            underTest.onDigitButtonDown(null)
 
             assertTrue(kosmos.fakeFalsingCollector.wasLastGestureAvoided())
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onDigiButtonDown_deliversKeyStandardToken() =
+        testScope.runTest {
+            underTest.onDigitButtonDown(null)
+
+            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.KEYPRESS_STANDARD)
+            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onBackspaceButtonPressed_deliversKeyDeleteToken() {
+        underTest.onBackspaceButtonPressed(null)
+
+        assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.KEYPRESS_DELETE)
+        assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onBackspaceButtonLongPressed_deliversLongPressToken() {
+        underTest.onBackspaceButtonLongPressed()
+
+        assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.LONG_PRESS)
+        assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
+    }
+
     private fun TestScope.switchToScene(toScene: SceneKey) {
         val currentScene by collectLastValue(sceneInteractor.currentScene)
         val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
index 19e7537..b8c30fe 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt
@@ -76,11 +76,7 @@
     /** Deliver MSDL feedback when the delete key of the pin bouncer is pressed */
     fun playDeleteKeyPressFeedback() = msdlPlayer.get().playToken(MSDLToken.KEYPRESS_DELETE)
 
-    /**
-     * Deliver MSDL feedback when the delete key of the pin bouncer is long-pressed
-     *
-     * @return whether MSDL feedback is allowed to play.
-     */
+    /** Deliver MSDL feedback when the delete key of the pin bouncer is long-pressed. */
     fun playDeleteKeyLongPressedFeedback() = msdlPlayer.get().playToken(MSDLToken.LONG_PRESS)
 
     /** Deliver MSDL feedback when a numpad key is pressed on the pin bouncer */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index c67b354..873d1b3 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.authentication.domain.interactor.AuthenticationResult
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.channels.Channel
@@ -42,6 +43,7 @@
 
     /** Name to use for performance tracing purposes. */
     val traceName: String,
+    protected val bouncerHapticPlayer: BouncerHapticPlayer? = null,
 ) : ExclusiveActivatable() {
 
     private val _animateFailure = MutableStateFlow(false)
@@ -80,6 +82,8 @@
                 return@collectLatest
             }
 
+            performAuthenticationHapticFeedback(authenticationResult)
+
             _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
             clearInput()
         }
@@ -112,20 +116,23 @@
     /** Returns the input entered so far. */
     protected abstract fun getInput(): List<Any>
 
+    /** Perform authentication result haptics */
+    private fun performAuthenticationHapticFeedback(result: AuthenticationResult) {
+        if (result == AuthenticationResult.SKIPPED) return
+
+        bouncerHapticPlayer?.playAuthenticationFeedback(
+            authenticationSucceeded = result == AuthenticationResult.SUCCEEDED
+        )
+    }
+
     /**
      * Attempts to authenticate the user using the current input value.
      *
      * @see BouncerInteractor.authenticate
      */
-    protected fun tryAuthenticate(
-        input: List<Any> = getInput(),
-        useAutoConfirm: Boolean = false,
-    ) {
+    protected fun tryAuthenticate(input: List<Any> = getInput(), useAutoConfirm: Boolean = false) {
         authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm))
     }
 
-    private data class AuthenticationRequest(
-        val input: List<Any>,
-        val useAutoConfirm: Boolean,
-    )
+    private data class AuthenticationRequest(val input: List<Any>, val useAutoConfirm: Boolean)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
index 0aada06..0bcb58d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.qualifiers.Application
@@ -61,6 +62,7 @@
     private val pinViewModelFactory: PinBouncerViewModel.Factory,
     private val patternViewModelFactory: PatternBouncerViewModel.Factory,
     private val passwordViewModelFactory: PasswordBouncerViewModel.Factory,
+    private val bouncerHapticPlayer: BouncerHapticPlayer,
 ) : ExclusiveActivatable() {
     private val _selectedUserImage = MutableStateFlow<Bitmap?>(null)
     val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow()
@@ -162,10 +164,7 @@
             }
 
             launch {
-                combine(
-                        userSwitcher.users,
-                        userSwitcher.menu,
-                    ) { users, actions ->
+                combine(userSwitcher.users, userSwitcher.menu) { users, actions ->
                         users.map { user ->
                             UserSwitcherDropdownItemViewModel(
                                 icon = Icon.Loaded(user.image, contentDescription = null),
@@ -178,7 +177,7 @@
                                     icon =
                                         Icon.Resource(
                                             action.iconResourceId,
-                                            contentDescription = null
+                                            contentDescription = null,
                                         ),
                                     text = Text.Resource(action.textResourceId),
                                     onClick = action.onClicked,
@@ -226,7 +225,7 @@
     }
 
     private fun getChildViewModel(
-        authenticationMethod: AuthenticationMethodModel,
+        authenticationMethod: AuthenticationMethodModel
     ): AuthMethodBouncerViewModel? {
         // If the current child view-model matches the authentication method, reuse it instead of
         // creating a new instance.
@@ -241,12 +240,14 @@
                     authenticationMethod = authenticationMethod,
                     onIntentionalUserInput = ::onIntentionalUserInput,
                     isInputEnabled = isInputEnabled,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             is AuthenticationMethodModel.Sim ->
                 pinViewModelFactory.create(
                     authenticationMethod = authenticationMethod,
                     onIntentionalUserInput = ::onIntentionalUserInput,
                     isInputEnabled = isInputEnabled,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             is AuthenticationMethodModel.Password ->
                 passwordViewModelFactory.create(
@@ -257,6 +258,7 @@
                 patternViewModelFactory.create(
                     onIntentionalUserInput = ::onIntentionalUserInput,
                     isInputEnabled = isInputEnabled,
+                    bouncerHapticPlayer = bouncerHapticPlayer,
                 )
             else -> null
         }
@@ -317,10 +319,7 @@
         return when {
             // The wipe dialog takes priority over the lockout dialog.
             wipeText != null ->
-                DialogViewModel(
-                    text = wipeText,
-                    onDismiss = { wipeDialogMessage.value = null },
-                )
+                DialogViewModel(text = wipeText, onDismiss = { wipeDialogMessage.value = null })
             lockoutText != null ->
                 DialogViewModel(
                     text = lockoutText,
@@ -338,7 +337,7 @@
     fun onKeyEvent(keyEvent: KeyEvent): Boolean {
         return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
             keyEvent.type,
-            keyEvent.nativeKeyEvent.keyCode
+            keyEvent.nativeKeyEvent.keyCode,
         ) ?: false
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 0a866b4..158f102 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -18,9 +18,11 @@
 
 import android.content.Context
 import android.util.TypedValue
+import android.view.View
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.res.R
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -35,7 +37,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 
 /** Holds UI state and handles user input for the pattern bouncer UI. */
@@ -44,6 +45,7 @@
 constructor(
     private val applicationContext: Context,
     interactor: BouncerInteractor,
+    @Assisted bouncerHapticPlayer: BouncerHapticPlayer,
     @Assisted isInputEnabled: StateFlow<Boolean>,
     @Assisted private val onIntentionalUserInput: () -> Unit,
 ) :
@@ -51,6 +53,7 @@
         interactor = interactor,
         isInputEnabled = isInputEnabled,
         traceName = "PatternBouncerViewModel",
+        bouncerHapticPlayer = bouncerHapticPlayer,
     ) {
 
     /** The number of columns in the dot grid. */
@@ -190,14 +193,7 @@
     private fun defaultDots(): List<PatternDotViewModel> {
         return buildList {
             (0 until columnCount).forEach { x ->
-                (0 until rowCount).forEach { y ->
-                    add(
-                        PatternDotViewModel(
-                            x = x,
-                            y = y,
-                        )
-                    )
-                }
+                (0 until rowCount).forEach { y -> add(PatternDotViewModel(x = x, y = y)) }
             }
         }
     }
@@ -207,14 +203,17 @@
         applicationContext.resources.getValue(
             com.android.internal.R.dimen.lock_pattern_dot_hit_factor,
             outValue,
-            true
+            true,
         )
         max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
     }
 
+    fun performDotFeedback(view: View?) = bouncerHapticPlayer?.playPatternDotFeedback(view)
+
     @AssistedFactory
     interface Factory {
         fun create(
+            bouncerHapticPlayer: BouncerHapticPlayer,
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
         ): PatternBouncerViewModel
@@ -231,7 +230,7 @@
  */
 private fun PatternDotViewModel.isOnLineSegment(
     first: PatternDotViewModel,
-    second: PatternDotViewModel
+    second: PatternDotViewModel,
 ): Boolean {
     val anotherPoint = this
     // No need to consider any points outside the bounds of two end points
@@ -253,14 +252,8 @@
     return (this in a..b) || (this in b..a)
 }
 
-data class PatternDotViewModel(
-    val x: Int,
-    val y: Int,
-) {
+data class PatternDotViewModel(val x: Int, val y: Int) {
     fun toCoordinate(): AuthenticationPatternCoordinate {
-        return AuthenticationPatternCoordinate(
-            x = x,
-            y = y,
-        )
+        return AuthenticationPatternCoordinate(x = x, y = y)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index da29c62..0cb4260 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -19,12 +19,14 @@
 package com.android.systemui.bouncer.ui.viewmodel
 
 import android.content.Context
+import android.view.HapticFeedbackConstants
 import android.view.KeyEvent.KEYCODE_0
 import android.view.KeyEvent.KEYCODE_9
 import android.view.KeyEvent.KEYCODE_DEL
 import android.view.KeyEvent.KEYCODE_NUMPAD_0
 import android.view.KeyEvent.KEYCODE_NUMPAD_9
 import android.view.KeyEvent.isConfirmKey
+import android.view.View
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType
 import com.android.keyguard.PinShapeAdapter
@@ -32,6 +34,7 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
 import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
 import com.android.systemui.res.R
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -56,6 +59,7 @@
     applicationContext: Context,
     interactor: BouncerInteractor,
     private val simBouncerInteractor: SimBouncerInteractor,
+    @Assisted bouncerHapticPlayer: BouncerHapticPlayer,
     @Assisted isInputEnabled: StateFlow<Boolean>,
     @Assisted private val onIntentionalUserInput: () -> Unit,
     @Assisted override val authenticationMethod: AuthenticationMethodModel,
@@ -64,6 +68,7 @@
         interactor = interactor,
         isInputEnabled = isInputEnabled,
         traceName = "PinBouncerViewModel",
+        bouncerHapticPlayer = bouncerHapticPlayer,
     ) {
     /**
      * Whether the sim-related UI in the pin view is showing.
@@ -126,10 +131,9 @@
                     .collect { _hintedPinLength.value = it }
             }
             launch {
-                combine(
-                        mutablePinInput,
-                        interactor.isAutoConfirmEnabled,
-                    ) { mutablePinEntries, isAutoConfirmEnabled ->
+                combine(mutablePinInput, interactor.isAutoConfirmEnabled) {
+                        mutablePinEntries,
+                        isAutoConfirmEnabled ->
                         computeBackspaceButtonAppearance(
                             pinInput = mutablePinEntries,
                             isAutoConfirmEnabled = isAutoConfirmEnabled,
@@ -183,8 +187,22 @@
         mutablePinInput.value = mutablePinInput.value.deleteLast()
     }
 
+    fun onBackspaceButtonPressed(view: View?) {
+        if (bouncerHapticPlayer?.isEnabled == true) {
+            bouncerHapticPlayer.playDeleteKeyPressFeedback()
+        } else {
+            view?.performHapticFeedback(
+                HapticFeedbackConstants.VIRTUAL_KEY,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+            )
+        }
+    }
+
     /** Notifies that the user long-pressed the backspace button. */
     fun onBackspaceButtonLongPressed() {
+        if (bouncerHapticPlayer?.isEnabled == true) {
+            bouncerHapticPlayer.playDeleteKeyLongPressedFeedback()
+        }
         clearInput()
     }
 
@@ -266,13 +284,24 @@
         }
     }
 
-    /** Notifies that the user has pressed down on a digit button. */
-    fun onDigitButtonDown() {
+    /**
+     * Notifies that the user has pressed down on a digit button. This function also performs haptic
+     * feedback on the view.
+     */
+    fun onDigitButtonDown(view: View?) {
         if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
             // Current PIN bouncer informs FalsingInteractor#avoidGesture() upon every Pin button
             // touch.
             super.onDown()
         }
+        if (bouncerHapticPlayer?.isEnabled == true) {
+            bouncerHapticPlayer.playNumpadKeyFeedback()
+        } else {
+            view?.performHapticFeedback(
+                HapticFeedbackConstants.VIRTUAL_KEY,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+            )
+        }
     }
 
     @AssistedFactory
@@ -281,6 +310,7 @@
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
             authenticationMethod: AuthenticationMethodModel,
+            bouncerHapticPlayer: BouncerHapticPlayer,
         ): PinBouncerViewModel
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
index 4b61a0d..088bb02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
@@ -25,6 +25,7 @@
 import androidx.test.filters.LargeTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.ui.viewmodel.patternBouncerViewModelFactory
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
 import com.android.systemui.lifecycle.activateIn
 import com.android.systemui.motion.createSysUiComposeMotionTestRule
 import com.android.systemui.testKosmos
@@ -55,6 +56,7 @@
         kosmos.patternBouncerViewModelFactory.create(
             isInputEnabled = MutableStateFlow(true).asStateFlow(),
             onIntentionalUserInput = {},
+            bouncerHapticPlayer = kosmos.bouncerHapticPlayer,
         )
 
     @Before
@@ -75,11 +77,11 @@
                     content = { play -> if (play) PatternBouncerUnderTest() },
                     ComposeRecordingSpec.until(
                         recordBefore = false,
-                        checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) }
+                        checkDone = { motionTestValueOfNode(MotionTestKeys.entryCompleted) },
                     ) {
                         feature(MotionTestKeys.dotAppearFadeIn, floatArray)
                         feature(MotionTestKeys.dotAppearMoveUp, floatArray)
-                    }
+                    },
                 )
 
             assertThat(motion).timeSeriesMatchesGolden()
@@ -100,7 +102,7 @@
                         viewModel.onDragEnd()
                         // Failure animation starts when animateFailure flips to true...
                         viewModel.animateFailure.takeWhile { !it }.collect {}
-                    }
+                    },
                 ) {
                     // ... and ends when the composable flips it back to false.
                     viewModel.animateFailure.takeWhile { it }.collect {}
@@ -111,7 +113,7 @@
                     content = { PatternBouncerUnderTest() },
                     ComposeRecordingSpec(failureAnimationMotionControl) {
                         feature(MotionTestKeys.dotScaling, floatArray)
-                    }
+                    },
                 )
             assertThat(motion).timeSeriesMatchesGolden()
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index 649e4e8..1b1d8c5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -25,6 +25,8 @@
 import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
 import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer
+import com.android.systemui.haptics.msdl.bouncerHapticPlayer
 import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -34,9 +36,7 @@
 import kotlinx.coroutines.flow.StateFlow
 
 val Kosmos.bouncerUserActionsViewModel by Fixture {
-    BouncerUserActionsViewModel(
-        bouncerInteractor = bouncerInteractor,
-    )
+    BouncerUserActionsViewModel(bouncerInteractor = bouncerInteractor)
 }
 
 val Kosmos.bouncerUserActionsViewModelFactory by Fixture {
@@ -59,6 +59,7 @@
         pinViewModelFactory = pinBouncerViewModelFactory,
         patternViewModelFactory = patternBouncerViewModelFactory,
         passwordViewModelFactory = passwordBouncerViewModelFactory,
+        bouncerHapticPlayer = bouncerHapticPlayer,
     )
 }
 
@@ -76,6 +77,7 @@
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
             authenticationMethod: AuthenticationMethodModel,
+            bouncerHapticPlayer: BouncerHapticPlayer,
         ): PinBouncerViewModel {
             return PinBouncerViewModel(
                 applicationContext = applicationContext,
@@ -84,6 +86,7 @@
                 isInputEnabled = isInputEnabled,
                 onIntentionalUserInput = onIntentionalUserInput,
                 authenticationMethod = authenticationMethod,
+                bouncerHapticPlayer = bouncerHapticPlayer,
             )
         }
     }
@@ -92,6 +95,7 @@
 val Kosmos.patternBouncerViewModelFactory by Fixture {
     object : PatternBouncerViewModel.Factory {
         override fun create(
+            bouncerHapticPlayer: BouncerHapticPlayer,
             isInputEnabled: StateFlow<Boolean>,
             onIntentionalUserInput: () -> Unit,
         ): PatternBouncerViewModel {
@@ -100,6 +104,7 @@
                 interactor = bouncerInteractor,
                 isInputEnabled = isInputEnabled,
                 onIntentionalUserInput = onIntentionalUserInput,
+                bouncerHapticPlayer = bouncerHapticPlayer,
             )
         }
     }