Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a1992368 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[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
parent 9d97f851
Loading
Loading
Loading
Loading
+2 −1
Original line number Original line Diff line number Diff line
@@ -82,6 +82,7 @@ fun DismissibleHorizontalPager(
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
    key: ((Int) -> Any)? = null,
    key: ((Int) -> Any)? = null,
    pageSpacing: Dp = 0.dp,
    pageSpacing: Dp = 0.dp,
    isFalseTouchDetected: Boolean,
    indicator: @Composable BoxScope.() -> Unit,
    indicator: @Composable BoxScope.() -> Unit,
    pageContent: @Composable PagerScope.(page: Int) -> Unit,
    pageContent: @Composable PagerScope.(page: Int) -> Unit,
) {
) {
@@ -142,7 +143,7 @@ fun DismissibleHorizontalPager(
    Box(modifier = modifier) {
    Box(modifier = modifier) {
        HorizontalPager(
        HorizontalPager(
            state = state.pagerState,
            state = state.pagerState,
            userScrollEnabled = state.isScrollingEnabled,
            userScrollEnabled = state.isScrollingEnabled && !isFalseTouchDetected,
            key = key,
            key = key,
            pageSpacing = pageSpacing,
            pageSpacing = pageSpacing,
            pageContent = pageContent,
            pageContent = pageContent,
+23 −2
Original line number Original line Diff line number Diff line
@@ -36,6 +36,8 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
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.hoverable
import androidx.compose.foundation.indication
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.DragInteraction
@@ -72,7 +74,9 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clip
@@ -89,6 +93,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Layout
@@ -202,6 +207,8 @@ private fun CardCarouselContent(
        ) {
        ) {
            viewModel.cards.size
            viewModel.cards.size
        }
        }
    var isFalseTouchDetected: Boolean by
        remember(behavior.isCarouselScrollFalseTouch) { mutableStateOf(false) }


    val roundedCornerShape = RoundedCornerShape(32.dp)
    val roundedCornerShape = RoundedCornerShape(32.dp)


@@ -229,7 +236,16 @@ private fun CardCarouselContent(
                )
                )
            }
            }
        },
        },
        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 ->
    ) { index ->
        Card(
        Card(
            viewModel = viewModel.cards[index],
            viewModel = viewModel.cards[index],
@@ -1084,7 +1100,12 @@ data class MediaUiBehavior(
    val isCarouselDismissible: Boolean = true,
    val isCarouselDismissible: Boolean = true,
    val isCarouselScrollingEnabled: Boolean = true,
    val isCarouselScrollingEnabled: Boolean = true,
    val carouselVisibility: MediaCarouselVisibility = MediaCarouselVisibility.WhenNotEmpty,
    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
@Stable
+72 −14
Original line number Original line Diff line number Diff line
@@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.ImageBitmap
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.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -31,6 +33,7 @@ import com.android.systemui.media.remedia.domain.interactor.MediaInteractor
import com.android.systemui.media.remedia.domain.model.MediaActionModel
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.MediaColorScheme
import com.android.systemui.media.remedia.shared.model.MediaSessionState
import com.android.systemui.media.remedia.shared.model.MediaSessionState
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.res.R
import dagger.assisted.Assisted
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedFactory
@@ -43,6 +46,7 @@ class MediaViewModel
@AssistedInject
@AssistedInject
constructor(
constructor(
    private val interactor: MediaInteractor,
    private val interactor: MediaInteractor,
    private val falsingSystem: FalsingSystem,
    @Assisted private val context: Context,
    @Assisted private val context: Context,
    @Assisted private val carouselVisibility: MediaCarouselVisibility,
    @Assisted private val carouselVisibility: MediaCarouselVisibility,
) : ExclusiveActivatable() {
) : ExclusiveActivatable() {
@@ -106,10 +110,12 @@ constructor(
                                    seekProgress = progress
                                    seekProgress = progress
                                },
                                },
                                onScrubFinished = {
                                onScrubFinished = {
                                    if (!falsingSystem.isFalseTouch(Classifier.MEDIA_SEEKBAR)) {
                                        interactor.seek(
                                        interactor.seek(
                                            sessionKey = session.key,
                                            sessionKey = session.key,
                                            to = (seekProgress * session.durationMs).roundToLong(),
                                            to = (seekProgress * session.durationMs).roundToLong(),
                                        )
                                        )
                                    }
                                    isScrubbing = false
                                    isScrubbing = false
                                },
                                },
                            )
                            )
@@ -139,20 +145,36 @@ constructor(
                                                R.string.controls_media_dismiss_button
                                                R.string.controls_media_dismiss_button
                                            ),
                                            ),
                                        onClick = {
                                        onClick = {
                                            falsingSystem.runIfNotFalseTap(
                                                FalsingManager.LOW_PENALTY
                                            ) {
                                                interactor.hide(session.key)
                                                interactor.hide(session.key)
                                                isGutsVisible = false
                                                isGutsVisible = false
                                            }
                                        },
                                        },
                                    )
                                    )
                                } else {
                                } else {
                                    MediaGutsButtonViewModel(
                                    MediaGutsButtonViewModel(
                                        text = context.getString(R.string.cancel),
                                        text = context.getString(R.string.cancel),
                                        onClick = { isGutsVisible = false },
                                        onClick = {
                                            falsingSystem.runIfNotFalseTap(
                                                FalsingManager.LOW_PENALTY
                                            ) {
                                                isGutsVisible = false
                                            }
                                        },
                                    )
                                    )
                                },
                                },
                            secondaryAction =
                            secondaryAction =
                                MediaGutsButtonViewModel(
                                MediaGutsButtonViewModel(
                                        text = context.getString(R.string.cancel),
                                        text = context.getString(R.string.cancel),
                                        onClick = { isGutsVisible = false },
                                        onClick = {
                                            falsingSystem.runIfNotFalseTap(
                                                FalsingManager.LOW_PENALTY
                                            ) {
                                                isGutsVisible = false
                                            }
                                        },
                                    )
                                    )
                                    .takeIf { session.canBeHidden },
                                    .takeIf { session.canBeHidden },
                            settingsButton =
                            settingsButton =
@@ -165,7 +187,11 @@ constructor(
                                                    res = R.string.controls_media_settings_button
                                                    res = R.string.controls_media_settings_button
                                                ),
                                                ),
                                        ),
                                        ),
                                    onClick = { interactor.openMediaSettings() },
                                    onClick = {
                                        falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) {
                                            interactor.openMediaSettings()
                                        }
                                    },
                                ),
                                ),
                            onLongClick = { isGutsVisible = false },
                            onLongClick = { isGutsVisible = false },
                        )
                        )
@@ -178,7 +204,12 @@ constructor(
                                icon = session.outputDevice.icon,
                                icon = session.outputDevice.icon,
                                text = session.outputDevice.name,
                                text = session.outputDevice.name,
                                onClick = {
                                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 @@ constructor(
                        return MediaSecondaryActionViewModel.Action(
                        return MediaSecondaryActionViewModel.Action(
                            icon = session.outputDevice.icon,
                            icon = session.outputDevice.icon,
                            onClick = {
                            onClick = {
                                falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) {
                                    // TODO(b/397989775): tell the UI to show the output switcher.
                                    // 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 =
                override val onClickLabel =
                    context.getString(R.string.controls_media_playing_item_description)
                    context.getString(R.string.controls_media_playing_item_description)
                override val onLongClick = { isGutsVisible = true }
                override val onLongClick = { isGutsVisible = true }
@@ -230,7 +265,14 @@ constructor(
                MediaPlayPauseActionViewModel(
                MediaPlayPauseActionViewModel(
                    state = mediaSessionState,
                    state = mediaSessionState,
                    icon = icon,
                    icon = icon,
                    onClick = onClick ?: {},
                    onClick =
                        onClick?.let {
                            {
                                falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) {
                                    it()
                                }
                            }
                        },
                )
                )
            is MediaActionModel.None,
            is MediaActionModel.None,
            is MediaActionModel.ReserveSpace -> null
            is MediaActionModel.ReserveSpace -> null
@@ -240,12 +282,28 @@ constructor(
    private fun MediaActionModel.toSecondaryActionViewModel(): MediaSecondaryActionViewModel {
    private fun MediaActionModel.toSecondaryActionViewModel(): MediaSecondaryActionViewModel {
        return when (this) {
        return when (this) {
            is MediaActionModel.Action ->
            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.ReserveSpace -> MediaSecondaryActionViewModel.ReserveSpace
            is MediaActionModel.None -> MediaSecondaryActionViewModel.None
            is MediaActionModel.None -> MediaSecondaryActionViewModel.None
        }
        }
    }
    }


    interface FalsingSystem {
        fun runIfNotFalseTap(@FalsingManager.Penalty penalty: Int, block: () -> Unit)

        fun isFalseTouch(@Classifier.InteractionType interactionType: Int): Boolean
    }

    @AssistedFactory
    @AssistedFactory
    interface Factory {
    interface Factory {
        fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel
        fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel