[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