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

Commit 3e388b26 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Fixes long-press on emergency button.

Note: the long-press still doesn't do anything because the code in the
BouncerActionButtonInteractor code path for long-press checks
EmergencyAffordanceManager.needsEmergencyAffordance that returns false
but I made sure that the long-press action _is_ triggered.

As it turns out, adding a combinedClickable to a Material Button doesn't
work because the Material Button already sets clickable on its Surface
which prevents the touch from ever getting to the combinedClickable.

Unfortunately, rolling my own simplified button was the only path
forward.

Fix: 369767936
Test: manually verified that a single click still opens the emergency
call activity
Test: manually verified that long-press triggers the right code path;
even if it's not currently working (it also doesn't work in
pre-flexiglass/legacy)
Flag: com.android.systemui.scene_container

Change-Id: I1e684ba2df012c0416b0f50723a82fbd9cb3b8f9
parent b7e31cfb
Loading
Loading
Loading
Loading
+72 −154
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
@@ -46,7 +47,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -64,6 +64,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onKeyEvent
@@ -72,6 +73,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
@@ -88,7 +92,6 @@ import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.transitions
import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
import com.android.systemui.bouncer.ui.BouncerDialogFactory
@@ -131,7 +134,7 @@ fun BouncerContent(
    layout: BouncerSceneLayout,
    viewModel: BouncerSceneContentViewModel,
    dialogFactory: BouncerDialogFactory,
    modifier: Modifier
    modifier: Modifier,
) {
    Box(
        // Allows the content within each of the layouts to react to the appearance and
@@ -140,31 +143,17 @@ fun BouncerContent(
        // Despite the keyboard only being part of the password bouncer, adding it at this level is
        // both necessary to properly handle the keyboard in all layouts and harmless in cases when
        // the keyboard isn't used (like the PIN or pattern auth methods).
        modifier = modifier.imePadding().onKeyEvent(viewModel::onKeyEvent),
        modifier = modifier.imePadding().onKeyEvent(viewModel::onKeyEvent)
    ) {
        when (layout) {
            BouncerSceneLayout.STANDARD_BOUNCER ->
                StandardLayout(
                    viewModel = viewModel,
                )
            BouncerSceneLayout.STANDARD_BOUNCER -> StandardLayout(viewModel = viewModel)
            BouncerSceneLayout.BESIDE_USER_SWITCHER ->
                BesideUserSwitcherLayout(
                    viewModel = viewModel,
                )
            BouncerSceneLayout.BELOW_USER_SWITCHER ->
                BelowUserSwitcherLayout(
                    viewModel = viewModel,
                )
            BouncerSceneLayout.SPLIT_BOUNCER ->
                SplitLayout(
                    viewModel = viewModel,
                )
                BesideUserSwitcherLayout(viewModel = viewModel)
            BouncerSceneLayout.BELOW_USER_SWITCHER -> BelowUserSwitcherLayout(viewModel = viewModel)
            BouncerSceneLayout.SPLIT_BOUNCER -> SplitLayout(viewModel = viewModel)
        }

        Dialog(
            bouncerViewModel = viewModel,
            dialogFactory = dialogFactory,
        )
        Dialog(bouncerViewModel = viewModel, dialogFactory = dialogFactory)
    }
}

@@ -173,31 +162,19 @@ fun BouncerContent(
 * authentication attempt, including all messaging UI (directives, reasoning, errors, etc.).
 */
@Composable
private fun StandardLayout(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
private fun StandardLayout(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
    val isHeightExpanded =
        LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded

    FoldAware(
        modifier =
            modifier.padding(
                start = 32.dp,
                top = 92.dp,
                end = 32.dp,
                bottom = 48.dp,
            ),
        modifier = modifier.padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 48.dp),
        viewModel = viewModel,
        aboveFold = {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.fillMaxWidth(),
            ) {
                StatusMessage(
                    viewModel = viewModel.message,
                    modifier = Modifier,
                )
                StatusMessage(viewModel = viewModel.message, modifier = Modifier)

                OutputArea(
                    viewModel = viewModel,
@@ -210,9 +187,7 @@ private fun StandardLayout(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.fillMaxWidth(),
            ) {
                Box(
                    modifier = Modifier.weight(1f),
                ) {
                Box(modifier = Modifier.weight(1f)) {
                    InputArea(
                        viewModel = viewModel,
                        pinButtonRowVerticalSpacing = 12.dp,
@@ -221,10 +196,7 @@ private fun StandardLayout(
                    )
                }

                ActionArea(
                    viewModel = viewModel,
                    modifier = Modifier.padding(top = 48.dp),
                )
                ActionArea(viewModel = viewModel, modifier = Modifier.padding(top = 48.dp))
            }
        },
    )
@@ -235,10 +207,7 @@ private fun StandardLayout(
 * by double-tapping on the side.
 */
@Composable
private fun SplitLayout(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
private fun SplitLayout(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
    val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle()

    Row(
@@ -248,12 +217,10 @@ private fun SplitLayout(
                .padding(
                    horizontal = 24.dp,
                    vertical = if (authMethod is PasswordBouncerViewModel) 24.dp else 48.dp,
                ),
                )
    ) {
        // Left side (in left-to-right locales).
        Box(
            modifier = Modifier.fillMaxHeight().weight(1f),
        ) {
        Box(modifier = Modifier.fillMaxHeight().weight(1f)) {
            when (authMethod) {
                is PinBouncerViewModel -> {
                    StatusMessage(
@@ -263,7 +230,7 @@ private fun SplitLayout(
                    OutputArea(
                        viewModel = viewModel,
                        modifier =
                            Modifier.align(Alignment.Center).sysuiResTag("bouncer_text_entry")
                            Modifier.align(Alignment.Center).sysuiResTag("bouncer_text_entry"),
                    )

                    ActionArea(
@@ -293,9 +260,7 @@ private fun SplitLayout(
        }

        // Right side (in right-to-left locales).
        Box(
            modifier = Modifier.fillMaxHeight().weight(1f),
        ) {
        Box(modifier = Modifier.fillMaxHeight().weight(1f)) {
            when (authMethod) {
                is PinBouncerViewModel,
                is PatternBouncerViewModel -> {
@@ -311,13 +276,11 @@ private fun SplitLayout(
                        horizontalAlignment = Alignment.CenterHorizontally,
                        modifier = Modifier.fillMaxWidth().align(Alignment.Center),
                    ) {
                        StatusMessage(
                            viewModel = viewModel.message,
                        )
                        StatusMessage(viewModel = viewModel.message)
                        OutputArea(
                            viewModel = viewModel,
                            modifier =
                                Modifier.padding(top = 24.dp).sysuiResTag("bouncer_text_entry")
                                Modifier.padding(top = 24.dp).sysuiResTag("bouncer_text_entry"),
                        )
                    }
                }
@@ -365,7 +328,7 @@ private fun BesideUserSwitcherLayout(
                .padding(
                    top = if (isHeightExpanded) 128.dp else 96.dp,
                    bottom = if (isHeightExpanded) 128.dp else 48.dp,
                ),
                )
    ) {
        LaunchedEffect(isSwapped) { swapAnimationEnd = false }
        val animatedOffset by
@@ -419,14 +382,12 @@ private fun BesideUserSwitcherLayout(
            aboveFold = {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier.fillMaxWidth()
                    modifier = Modifier.fillMaxWidth(),
                ) {
                    StatusMessage(
                        viewModel = viewModel.message,
                    )
                    StatusMessage(viewModel = viewModel.message)
                    OutputArea(
                        viewModel = viewModel,
                        modifier = Modifier.padding(top = 24.dp).sysuiResTag("bouncer_text_entry")
                        modifier = Modifier.padding(top = 24.dp).sysuiResTag("bouncer_text_entry"),
                    )
                }
            },
@@ -444,7 +405,7 @@ private fun BesideUserSwitcherLayout(
                    Box(
                        modifier =
                            Modifier.weight(1f)
                                .padding(top = (if (addSpacingBetweenOutputAndInput) 24 else 0).dp),
                                .padding(top = (if (addSpacingBetweenOutputAndInput) 24 else 0).dp)
                    ) {
                        InputArea(
                            viewModel = viewModel,
@@ -470,16 +431,8 @@ private fun BelowUserSwitcherLayout(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier =
            modifier.padding(
                vertical = 128.dp,
            )
    ) {
        UserSwitcher(
            viewModel = viewModel,
            modifier = Modifier.fillMaxWidth(),
        )
    Column(modifier = modifier.padding(vertical = 128.dp)) {
        UserSwitcher(viewModel = viewModel, modifier = Modifier.fillMaxWidth())

        Spacer(Modifier.weight(1f))

@@ -488,9 +441,7 @@ private fun BelowUserSwitcherLayout(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.fillMaxWidth(),
            ) {
                StatusMessage(
                    viewModel = viewModel.message,
                )
                StatusMessage(viewModel = viewModel.message)
                OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))

                InputArea(
@@ -500,10 +451,7 @@ private fun BelowUserSwitcherLayout(
                    modifier = Modifier.padding(top = 128.dp),
                )

                ActionArea(
                    viewModel = viewModel,
                    modifier = Modifier.padding(top = 48.dp),
                )
                ActionArea(viewModel = viewModel, modifier = Modifier.padding(top = 48.dp))
            }
        }
    }
@@ -533,19 +481,11 @@ private fun FoldAware(

    SceneTransitionLayout(state, modifier = modifier) {
        scene(SceneKeys.ContiguousSceneKey) {
            FoldableScene(
                aboveFold = aboveFold,
                belowFold = belowFold,
                isSplit = false,
            )
            FoldableScene(aboveFold = aboveFold, belowFold = belowFold, isSplit = false)
        }

        scene(SceneKeys.SplitSceneKey) {
            FoldableScene(
                aboveFold = aboveFold,
                belowFold = belowFold,
                isSplit = true,
            )
            FoldableScene(aboveFold = aboveFold, belowFold = belowFold, isSplit = true)
        }
    }
}
@@ -562,9 +502,7 @@ private fun SceneScope.FoldableScene(
            R.dimen.motion_layout_half_fold_bouncer_height_ratio
        )

    Column(
        modifier = modifier.fillMaxHeight(),
    ) {
    Column(modifier = modifier.fillMaxHeight()) {
        // Content above the fold, when split on a foldable device in a "table top" posture:
        Box(
            modifier =
@@ -575,7 +513,7 @@ private fun SceneScope.FoldableScene(
                        } else {
                            Modifier
                        }
                    ),
                    )
        ) {
            aboveFold()
        }
@@ -590,7 +528,7 @@ private fun SceneScope.FoldableScene(
                        } else {
                            1f
                        }
                    ),
                    )
        ) {
            belowFold()
        }
@@ -598,10 +536,7 @@ private fun SceneScope.FoldableScene(
}

@Composable
private fun StatusMessage(
    viewModel: BouncerMessageViewModel,
    modifier: Modifier = Modifier,
) {
private fun StatusMessage(viewModel: BouncerMessageViewModel, modifier: Modifier = Modifier) {
    val message: MessageViewModel? by viewModel.message.collectAsStateWithLifecycle()

    DisposableEffect(Unit) {
@@ -634,7 +569,7 @@ private fun StatusMessage(
                    fontSize = 14.sp,
                    lineHeight = 20.sp,
                    overflow = TextOverflow.Ellipsis,
                    maxLines = 2
                    maxLines = 2,
                )
            }
        }
@@ -647,22 +582,19 @@ private fun StatusMessage(
 * For example, this can be the PIN shapes or password text field.
 */
@Composable
private fun OutputArea(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
private fun OutputArea(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
    val authMethodViewModel: AuthMethodBouncerViewModel? by
        viewModel.authMethodViewModel.collectAsStateWithLifecycle()
    when (val nonNullViewModel = authMethodViewModel) {
        is PinBouncerViewModel ->
            PinInputDisplay(
                viewModel = nonNullViewModel,
                modifier = modifier.sysuiResTag("bouncer_text_entry")
                modifier = modifier.sysuiResTag("bouncer_text_entry"),
            )
        is PasswordBouncerViewModel ->
            PasswordBouncer(
                viewModel = nonNullViewModel,
                modifier = modifier.sysuiResTag("bouncer_text_entry")
                modifier = modifier.sysuiResTag("bouncer_text_entry"),
            )
        else -> Unit
    }
@@ -703,10 +635,7 @@ private fun InputArea(
}

@Composable
private fun ActionArea(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
private fun ActionArea(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
    val actionButton: BouncerActionButtonModel? by
        viewModel.actionButton.collectAsStateWithLifecycle()
    val appearFadeInAnimatable = remember { Animatable(0f) }
@@ -722,7 +651,7 @@ private fun ActionArea(
                        durationMillis = 450,
                        delayMillis = 133,
                        easing = Easings.LegacyDecelerate,
                    )
                    ),
            )
        }
        LaunchedEffect(Unit) {
@@ -733,42 +662,38 @@ private fun ActionArea(
                        durationMillis = 450,
                        delayMillis = 133,
                        easing = Easings.StandardDecelerate,
                    )
                    ),
            )
        }

        Box(
            modifier =
                modifier.graphicsLayer {
                modifier
                    .graphicsLayer {
                        // Translate the button up from an initially pushed-down position:
                    translationY = (1 - appearMoveAnimatable.value) * appearAnimationInitialOffset
                        translationY =
                            (1 - appearMoveAnimatable.value) * appearAnimationInitialOffset
                        // Fade the button in:
                        alpha = appearFadeInAnimatable.value
                },
        ) {
            Button(
                onClick = actionButtonViewModel.onClick,
                modifier =
                    Modifier.height(56.dp).thenIf(actionButtonViewModel.onLongClick != null) {
                        Modifier.combinedClickable(
                            onClick = actionButtonViewModel.onClick,
                            onLongClick = actionButtonViewModel.onLongClick,
                    }
                    .height(56.dp)
                    .clip(ButtonDefaults.shape)
                    .background(color = MaterialTheme.colorScheme.tertiaryContainer)
                    .semantics { role = Role.Button }
                    .combinedClickable(
                        onClick = { actionButtonViewModel.onClick() },
                        onLongClick = actionButtonViewModel.onLongClick?.let { { it.invoke() } },
                    )
                    },
                colors =
                    ButtonDefaults.buttonColors(
                        containerColor = MaterialTheme.colorScheme.tertiaryContainer,
                        contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
                    ),
        ) {
            Text(
                text = actionButtonViewModel.label,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onTertiaryContainer,
                modifier = Modifier.align(Alignment.Center).padding(ButtonDefaults.ContentPadding),
            )
        }
    }
}
}

@Composable
private fun Dialog(
@@ -800,15 +725,10 @@ private fun Dialog(

/** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
@Composable
private fun UserSwitcher(
    viewModel: BouncerSceneContentViewModel,
    modifier: Modifier = Modifier,
) {
private fun UserSwitcher(viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier) {
    if (!viewModel.isUserSwitcherVisible) {
        // Take up the same space as the user switcher normally would, but with nothing inside it.
        Box(
            modifier = modifier,
        )
        Box(modifier = modifier)
        return
    }

@@ -891,7 +811,7 @@ private fun UserSwitcherDropdownMenu(
    MaterialTheme(
        colorScheme =
            MaterialTheme.colorScheme.copy(
                surface = MaterialTheme.colorScheme.surfaceContainerHighest,
                surface = MaterialTheme.colorScheme.surfaceContainerHighest
            ),
        shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(28.dp)),
    ) {
@@ -932,9 +852,7 @@ private fun UserSwitcherDropdownMenu(
 * Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of
 * the two reaches a stopping point but `0` in the middle of the transition.
 */
private fun animatedAlpha(
    offset: Float,
): Float {
private fun animatedAlpha(offset: Float): Float {
    // Describes a curve that is made of two parabolic U-shaped curves mirrored horizontally around
    // the y-axis. The U on the left runs between x = -1 and x = 0 while the U on the right runs
    // between x = 0 and x = 1.
+0 −2
Original line number Diff line number Diff line
@@ -119,8 +119,6 @@ constructor(
            onLongClick = {
                if (emergencyAffordanceManager.needsEmergencyAffordance()) {
                    prepareToPerformAction()

                    // TODO(b/369767936): Check that !longPressWasDragged before invoking.
                    emergencyAffordanceManager.performEmergencyCall()
                }
            },