[Media] Falsing protection.

Bug: 397989775
Test: manually verified with a fake falsing checkbox in the compose
gallery app. I checked that no clicks work on any button (play/pause,
prev/next, additional actions, output switcher chip), on the card
itself, or on the buttons of the guts. I also checked that swiping
between cards and scrubbing the seekbar doesn't stick.
Flag: EXEMPT code remains unused for now

Change-Id: Iba661a073bcb6cb45687ae7cd7b5277e43ed4e89
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
index fea5b326..8df916f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
@@ -82,6 +82,7 @@
     modifier: Modifier = Modifier,
     key: ((Int) -> Any)? = null,
     pageSpacing: Dp = 0.dp,
+    isFalseTouchDetected: Boolean,
     indicator: @Composable BoxScope.() -> Unit,
     pageContent: @Composable PagerScope.(page: Int) -> Unit,
 ) {
@@ -142,7 +143,7 @@
     Box(modifier = modifier) {
         HorizontalPager(
             state = state.pagerState,
-            userScrollEnabled = state.isScrollingEnabled,
+            userScrollEnabled = state.isScrollingEnabled && !isFalseTouchDetected,
             key = key,
             pageSpacing = pageSpacing,
             pageContent = pageContent,
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
index 9eb55a8..3c18637 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
@@ -36,6 +36,8 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.hoverable
 import androidx.compose.foundation.indication
 import androidx.compose.foundation.interaction.DragInteraction
@@ -72,7 +74,9 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
@@ -89,6 +93,7 @@
 import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.drawscope.translate
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.Layout
@@ -202,6 +207,8 @@
         ) {
             viewModel.cards.size
         }
+    var isFalseTouchDetected: Boolean by
+        remember(behavior.isCarouselScrollFalseTouch) { mutableStateOf(false) }
 
     val roundedCornerShape = RoundedCornerShape(32.dp)
 
@@ -229,7 +236,16 @@
                 )
             }
         },
-        modifier = modifier.padding(8.dp).clip(roundedCornerShape),
+        isFalseTouchDetected = isFalseTouchDetected,
+        modifier =
+            modifier.padding(8.dp).clip(roundedCornerShape).pointerInput(behavior) {
+                if (behavior.isCarouselScrollFalseTouch != null) {
+                    awaitEachGesture {
+                        awaitFirstDown(false, PointerEventPass.Initial)
+                        isFalseTouchDetected = behavior.isCarouselScrollFalseTouch.invoke()
+                    }
+                }
+            },
     ) { index ->
         Card(
             viewModel = viewModel.cards[index],
@@ -1084,7 +1100,12 @@
     val isCarouselDismissible: Boolean = true,
     val isCarouselScrollingEnabled: Boolean = true,
     val carouselVisibility: MediaCarouselVisibility = MediaCarouselVisibility.WhenNotEmpty,
-    val isFalsingProtectionNeeded: Boolean = false,
+    /**
+     * If provided, this callback will be consulted at the beginning of each carousel scroll gesture
+     * to see if the falsing system thinks that it's a false touch. If it then returns `true`, the
+     * scroll will be canceled.
+     */
+    val isCarouselScrollFalseTouch: (() -> Boolean)? = null,
 )
 
 @Stable
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
index b4f3d27..61444e5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
@@ -24,6 +24,8 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.graphics.ImageBitmap
+import com.android.systemui.classifier.Classifier
+import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -31,6 +33,7 @@
 import com.android.systemui.media.remedia.domain.model.MediaActionModel
 import com.android.systemui.media.remedia.shared.model.MediaColorScheme
 import com.android.systemui.media.remedia.shared.model.MediaSessionState
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -43,6 +46,7 @@
 @AssistedInject
 constructor(
     private val interactor: MediaInteractor,
+    private val falsingSystem: FalsingSystem,
     @Assisted private val context: Context,
     @Assisted private val carouselVisibility: MediaCarouselVisibility,
 ) : ExclusiveActivatable() {
@@ -106,10 +110,12 @@
                                     seekProgress = progress
                                 },
                                 onScrubFinished = {
-                                    interactor.seek(
-                                        sessionKey = session.key,
-                                        to = (seekProgress * session.durationMs).roundToLong(),
-                                    )
+                                    if (!falsingSystem.isFalseTouch(Classifier.MEDIA_SEEKBAR)) {
+                                        interactor.seek(
+                                            sessionKey = session.key,
+                                            to = (seekProgress * session.durationMs).roundToLong(),
+                                        )
+                                    }
                                     isScrubbing = false
                                 },
                             )
@@ -139,20 +145,36 @@
                                                 R.string.controls_media_dismiss_button
                                             ),
                                         onClick = {
-                                            interactor.hide(session.key)
-                                            isGutsVisible = false
+                                            falsingSystem.runIfNotFalseTap(
+                                                FalsingManager.LOW_PENALTY
+                                            ) {
+                                                interactor.hide(session.key)
+                                                isGutsVisible = false
+                                            }
                                         },
                                     )
                                 } else {
                                     MediaGutsButtonViewModel(
                                         text = context.getString(R.string.cancel),
-                                        onClick = { isGutsVisible = false },
+                                        onClick = {
+                                            falsingSystem.runIfNotFalseTap(
+                                                FalsingManager.LOW_PENALTY
+                                            ) {
+                                                isGutsVisible = false
+                                            }
+                                        },
                                     )
                                 },
                             secondaryAction =
                                 MediaGutsButtonViewModel(
                                         text = context.getString(R.string.cancel),
-                                        onClick = { isGutsVisible = false },
+                                        onClick = {
+                                            falsingSystem.runIfNotFalseTap(
+                                                FalsingManager.LOW_PENALTY
+                                            ) {
+                                                isGutsVisible = false
+                                            }
+                                        },
                                     )
                                     .takeIf { session.canBeHidden },
                             settingsButton =
@@ -165,7 +187,11 @@
                                                     res = R.string.controls_media_settings_button
                                                 ),
                                         ),
-                                    onClick = { interactor.openMediaSettings() },
+                                    onClick = {
+                                        falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) {
+                                            interactor.openMediaSettings()
+                                        }
+                                    },
                                 ),
                             onLongClick = { isGutsVisible = false },
                         )
@@ -178,7 +204,12 @@
                                 icon = session.outputDevice.icon,
                                 text = session.outputDevice.name,
                                 onClick = {
-                                    // TODO(b/397989775): tell the UI to show the output switcher.
+                                    falsingSystem.runIfNotFalseTap(
+                                        FalsingManager.MODERATE_PENALTY
+                                    ) {
+                                        // TODO(b/397989775): tell the UI to show the output
+                                        // switcher.
+                                    }
                                 },
                             )
                         )
@@ -189,12 +220,16 @@
                         return MediaSecondaryActionViewModel.Action(
                             icon = session.outputDevice.icon,
                             onClick = {
-                                // TODO(b/397989775): tell the UI to show the output switcher.
+                                falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) {
+                                    // TODO(b/397989775): tell the UI to show the output switcher.
+                                }
                             },
                         )
                     }
 
-                override val onClick = session.onClick
+                override val onClick = {
+                    falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) { session.onClick() }
+                }
                 override val onClickLabel =
                     context.getString(R.string.controls_media_playing_item_description)
                 override val onLongClick = { isGutsVisible = true }
@@ -230,7 +265,14 @@
                 MediaPlayPauseActionViewModel(
                     state = mediaSessionState,
                     icon = icon,
-                    onClick = onClick ?: {},
+                    onClick =
+                        onClick?.let {
+                            {
+                                falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) {
+                                    it()
+                                }
+                            }
+                        },
                 )
             is MediaActionModel.None,
             is MediaActionModel.ReserveSpace -> null
@@ -240,12 +282,28 @@
     private fun MediaActionModel.toSecondaryActionViewModel(): MediaSecondaryActionViewModel {
         return when (this) {
             is MediaActionModel.Action ->
-                MediaSecondaryActionViewModel.Action(icon = icon, onClick = onClick)
+                MediaSecondaryActionViewModel.Action(
+                    icon = icon,
+                    onClick =
+                        onClick?.let {
+                            {
+                                falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) {
+                                    it()
+                                }
+                            }
+                        },
+                )
             is MediaActionModel.ReserveSpace -> MediaSecondaryActionViewModel.ReserveSpace
             is MediaActionModel.None -> MediaSecondaryActionViewModel.None
         }
     }
 
+    interface FalsingSystem {
+        fun runIfNotFalseTap(@FalsingManager.Penalty penalty: Int, block: () -> Unit)
+
+        fun isFalseTouch(@Classifier.InteractionType interactionType: Int): Boolean
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel