Loading packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt +2 −1 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ fun DismissibleHorizontalPager( modifier: Modifier = Modifier, key: ((Int) -> Any)? = null, pageSpacing: Dp = 0.dp, isFalseTouchDetected: Boolean, indicator: @Composable BoxScope.() -> Unit, pageContent: @Composable PagerScope.(page: Int) -> Unit, ) { Loading Loading @@ -142,7 +143,7 @@ fun DismissibleHorizontalPager( Box(modifier = modifier) { HorizontalPager( state = state.pagerState, userScrollEnabled = state.isScrollingEnabled, userScrollEnabled = state.isScrollingEnabled && !isFalseTouchDetected, key = key, pageSpacing = pageSpacing, pageContent = pageContent, Loading packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +23 −2 Original line number Diff line number Diff line Loading @@ -36,6 +36,8 @@ import androidx.compose.foundation.Image 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 Loading Loading @@ -72,7 +74,9 @@ import androidx.compose.runtime.Stable 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 Loading @@ -89,6 +93,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke 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 Loading Loading @@ -202,6 +207,8 @@ private fun CardCarouselContent( ) { viewModel.cards.size } var isFalseTouchDetected: Boolean by remember(behavior.isCarouselScrollFalseTouch) { mutableStateOf(false) } val roundedCornerShape = RoundedCornerShape(32.dp) Loading Loading @@ -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 -> Card( viewModel = viewModel.cards[index], Loading Loading @@ -1084,7 +1100,12 @@ data class MediaUiBehavior( 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 Loading packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt +72 −14 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableIntStateOf 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 Loading @@ -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.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 Loading @@ -43,6 +46,7 @@ class MediaViewModel @AssistedInject constructor( private val interactor: MediaInteractor, private val falsingSystem: FalsingSystem, @Assisted private val context: Context, @Assisted private val carouselVisibility: MediaCarouselVisibility, ) : ExclusiveActivatable() { Loading Loading @@ -106,10 +110,12 @@ constructor( seekProgress = progress }, onScrubFinished = { if (!falsingSystem.isFalseTouch(Classifier.MEDIA_SEEKBAR)) { interactor.seek( sessionKey = session.key, to = (seekProgress * session.durationMs).roundToLong(), ) } isScrubbing = false }, ) Loading Loading @@ -139,20 +145,36 @@ constructor( R.string.controls_media_dismiss_button ), onClick = { 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 = Loading @@ -165,7 +187,11 @@ constructor( res = R.string.controls_media_settings_button ), ), onClick = { interactor.openMediaSettings() }, onClick = { falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) { interactor.openMediaSettings() } }, ), onLongClick = { isGutsVisible = false }, ) Loading @@ -178,7 +204,12 @@ constructor( 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. } }, ) ) Loading @@ -189,12 +220,16 @@ constructor( return MediaSecondaryActionViewModel.Action( icon = session.outputDevice.icon, onClick = { 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 } Loading Loading @@ -230,7 +265,14 @@ constructor( MediaPlayPauseActionViewModel( state = mediaSessionState, icon = icon, onClick = onClick ?: {}, onClick = onClick?.let { { falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) { it() } } }, ) is MediaActionModel.None, is MediaActionModel.ReserveSpace -> null Loading @@ -240,12 +282,28 @@ constructor( 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 Loading Loading
packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt +2 −1 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ fun DismissibleHorizontalPager( modifier: Modifier = Modifier, key: ((Int) -> Any)? = null, pageSpacing: Dp = 0.dp, isFalseTouchDetected: Boolean, indicator: @Composable BoxScope.() -> Unit, pageContent: @Composable PagerScope.(page: Int) -> Unit, ) { Loading Loading @@ -142,7 +143,7 @@ fun DismissibleHorizontalPager( Box(modifier = modifier) { HorizontalPager( state = state.pagerState, userScrollEnabled = state.isScrollingEnabled, userScrollEnabled = state.isScrollingEnabled && !isFalseTouchDetected, key = key, pageSpacing = pageSpacing, pageContent = pageContent, Loading
packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +23 −2 Original line number Diff line number Diff line Loading @@ -36,6 +36,8 @@ import androidx.compose.foundation.Image 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 Loading Loading @@ -72,7 +74,9 @@ import androidx.compose.runtime.Stable 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 Loading @@ -89,6 +93,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke 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 Loading Loading @@ -202,6 +207,8 @@ private fun CardCarouselContent( ) { viewModel.cards.size } var isFalseTouchDetected: Boolean by remember(behavior.isCarouselScrollFalseTouch) { mutableStateOf(false) } val roundedCornerShape = RoundedCornerShape(32.dp) Loading Loading @@ -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 -> Card( viewModel = viewModel.cards[index], Loading Loading @@ -1084,7 +1100,12 @@ data class MediaUiBehavior( 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 Loading
packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt +72 −14 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableIntStateOf 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 Loading @@ -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.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 Loading @@ -43,6 +46,7 @@ class MediaViewModel @AssistedInject constructor( private val interactor: MediaInteractor, private val falsingSystem: FalsingSystem, @Assisted private val context: Context, @Assisted private val carouselVisibility: MediaCarouselVisibility, ) : ExclusiveActivatable() { Loading Loading @@ -106,10 +110,12 @@ constructor( seekProgress = progress }, onScrubFinished = { if (!falsingSystem.isFalseTouch(Classifier.MEDIA_SEEKBAR)) { interactor.seek( sessionKey = session.key, to = (seekProgress * session.durationMs).roundToLong(), ) } isScrubbing = false }, ) Loading Loading @@ -139,20 +145,36 @@ constructor( R.string.controls_media_dismiss_button ), onClick = { 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 = Loading @@ -165,7 +187,11 @@ constructor( res = R.string.controls_media_settings_button ), ), onClick = { interactor.openMediaSettings() }, onClick = { falsingSystem.runIfNotFalseTap(FalsingManager.LOW_PENALTY) { interactor.openMediaSettings() } }, ), onLongClick = { isGutsVisible = false }, ) Loading @@ -178,7 +204,12 @@ constructor( 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. } }, ) ) Loading @@ -189,12 +220,16 @@ constructor( return MediaSecondaryActionViewModel.Action( icon = session.outputDevice.icon, onClick = { 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 } Loading Loading @@ -230,7 +265,14 @@ constructor( MediaPlayPauseActionViewModel( state = mediaSessionState, icon = icon, onClick = onClick ?: {}, onClick = onClick?.let { { falsingSystem.runIfNotFalseTap(FalsingManager.MODERATE_PENALTY) { it() } } }, ) is MediaActionModel.None, is MediaActionModel.ReserveSpace -> null Loading @@ -240,12 +282,28 @@ constructor( 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 Loading