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

Commit 1732157a authored by Lucas Silva's avatar Lucas Silva
Browse files

Show unlock prompt when user interacts with the hub

This change also addresses an existing issue where tapping on the bottom
of the hub doesn't trigger customization or log the user interaction to
bump the screen timeout. This is because the hub composable no longer
takes up the entire screen, but is constrained to the lock icon. This
change therefore moves the touch handling logic out of the hub
composable and into the parent, so it can intercept touches across the
entire screen.

The widget selection logic is maintained in the hub for now to minimize
any behavior changes. In the future, the widget selection logic should
be moved to the actual widget views themselves.

Fixes: 340519071
Test: flashed device and verified long pressing on any blank space will
trigger customize prompt. Also, long pressing on a widget will trigger
customize prompt and pre-select that widget. Tapping on any blank space
will also trigger the unlock prompt now.
Flag: com.android.systemui.communal_hub

Change-Id: I1784cfc4c7bcead1efa8fc82e91a7d0e0f43e41d
parent 02a7f258
Loading
Loading
Loading
Loading
+69 −63
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.compose.animation.scene.SceneScope
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler
import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection
import com.android.systemui.communal.ui.compose.section.CommunalPopupSection
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -44,14 +45,17 @@ constructor(
    private val lockSection: LockSection,
    private val bottomAreaSection: BottomAreaSection,
    private val ambientStatusBarSection: AmbientStatusBarSection,
    private val communalPopupSection: CommunalPopupSection,
) {

    @Composable
    fun SceneScope.Content(modifier: Modifier = Modifier) {
        CommunalTouchableSurface(viewModel = viewModel, modifier = modifier) {
            Layout(
            modifier = modifier.fillMaxSize(),
                modifier = Modifier.fillMaxSize(),
                content = {
                    Box(modifier = Modifier.fillMaxSize()) {
                        with(communalPopupSection) { Popup() }
                        with(ambientStatusBarSection) {
                            AmbientStatusBar(modifier = Modifier.fillMaxWidth())
                        }
@@ -97,7 +101,8 @@ constructor(
                val bottomAreaPlaceable =
                    bottomAreaMeasurable.measure(
                        noMinConstraints.copy(
                        maxHeight = (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
                            maxHeight =
                                (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
                        )
                    )

@@ -123,3 +128,4 @@ constructor(
            }
        }
    }
}
+19 −167
Original line number Diff line number Diff line
@@ -25,7 +25,6 @@ import android.widget.FrameLayout
import android.widget.RemoteViews
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
@@ -70,7 +69,6 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
@@ -102,10 +100,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.motionEventSpy
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -125,14 +120,12 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.layout.WindowMetricsCalculator
import com.android.compose.animation.Easings.Emphasized
@@ -151,7 +144,6 @@ import com.android.systemui.communal.ui.compose.extensions.observeTaps
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.PopupType
import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.res.R
@@ -171,7 +163,6 @@ fun CommunalHub(
) {
    val communalContent by
        viewModel.communalContent.collectAsStateWithLifecycle(initialValue = emptyList())
    val currentPopup by viewModel.currentPopup.collectAsStateWithLifecycle(initialValue = null)
    var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
    var toolbarSize: IntSize? by remember { mutableStateOf(null) }
    var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
@@ -225,31 +216,17 @@ fun CommunalHub(
                                gridCoordinates?.let {
                                    offset - it.positionInWindow() - contentOffset
                                }
                                val index =
                                    adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
                                // Display the button only when the gesture initiates from widgets,
                                // the CTA tile, or an empty area on the screen. UMO/smartspace have
                                // their own long-press handlers. To prevent user confusion, we
                                // should
                                // not display this button.
                                if (
                                    index == null ||
                                        communalContent[index].isWidgetContent() ||
                                        communalContent[index] is
                                            CommunalContentModel.CtaTileInViewMode
                                ) {
                                    viewModel.onShowCustomizeWidgetButton()
                                }
                                val key =
                                    index?.let { keyAtIndexIfEditable(communalContent, index) }
                            val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
                            val key = index?.let { keyAtIndexIfEditable(communalContent, index) }
                            // Handle long-click on widgets and set the selected index
                            // correctly. We only handle widgets here because long click on
                            // empty spaces is handled by CommunalPopupSection.
                            if (key != null) {
                                viewModel.onLongClick()
                                viewModel.setSelectedKey(key)
                            }
                        }
                        .onPreviewKeyEvent {
                            onKeyEvent(viewModel)
                            false
                    }
                        .motionEventSpy { onMotionEvent(viewModel) }
                },
    ) {
        AccessibilityContainer(viewModel) {
@@ -342,22 +319,6 @@ fun CommunalHub(
                )
            }
        }
        if (currentPopup == PopupType.CtaTile) {
            PopupOnDismissCtaTile(viewModel::onHidePopup)
        }

        AnimatedVisibility(
            visible = currentPopup == PopupType.CustomizeWidgetButton,
            modifier = Modifier.fillMaxSize()
        ) {
            ButtonToEditWidgets(
                onClick = {
                    viewModel.onHidePopup()
                    viewModel.onOpenWidgetEditor(selectedKey.value)
                },
                onHide = { viewModel.onHidePopup() }
            )
        }

        if (viewModel is CommunalViewModel && dialogFactory != null) {
            val isEnableWidgetDialogShowing by
@@ -413,14 +374,6 @@ fun CommunalHub(
    }
}

private fun onKeyEvent(viewModel: BaseCommunalViewModel) {
    viewModel.signalUserInteraction()
}

private fun onMotionEvent(viewModel: BaseCommunalViewModel) {
    viewModel.signalUserInteraction()
}

@Composable
private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) {
    val colors = LocalAndroidColorScheme.current
@@ -820,107 +773,6 @@ private fun ToolbarButton(
    }
}

@Composable
private fun AnimatedVisibilityScope.ButtonToEditWidgets(
    onClick: () -> Unit,
    onHide: () -> Unit,
) {
    Popup(
        alignment = Alignment.TopCenter,
        offset = IntOffset(0, 40),
        onDismissRequest = onHide,
    ) {
        val colors = LocalAndroidColorScheme.current
        Button(
            modifier =
                Modifier.height(56.dp)
                    .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) }
                    .animateEnterExit(
                        enter =
                            fadeIn(
                                initialAlpha = 0f,
                                animationSpec = tween(durationMillis = 83, easing = LinearEasing)
                            ),
                        exit =
                            fadeOut(
                                animationSpec =
                                    tween(
                                        durationMillis = 83,
                                        delayMillis = 167,
                                        easing = LinearEasing
                                    )
                            )
                    )
                    .background(colors.secondary, RoundedCornerShape(50.dp)),
            onClick = onClick,
        ) {
            Row(
                modifier =
                    Modifier.animateEnterExit(
                        enter =
                            fadeIn(
                                animationSpec =
                                    tween(
                                        durationMillis = 167,
                                        delayMillis = 83,
                                        easing = LinearEasing
                                    )
                            ),
                        exit =
                            fadeOut(
                                animationSpec = tween(durationMillis = 167, easing = LinearEasing)
                            )
                    )
            ) {
                Icon(
                    imageVector = Icons.Outlined.Widgets,
                    contentDescription = stringResource(R.string.button_to_configure_widgets_text),
                    tint = colors.onSecondary,
                    modifier = Modifier.size(20.dp)
                )
                Spacer(modifier = Modifier.size(8.dp))
                Text(
                    text = stringResource(R.string.button_to_configure_widgets_text),
                    style = MaterialTheme.typography.titleSmall,
                    color = colors.onSecondary
                )
            }
        }
    }
}

@Composable
private fun PopupOnDismissCtaTile(onHidePopup: () -> Unit) {
    Popup(
        alignment = Alignment.TopCenter,
        offset = IntOffset(0, 40),
        onDismissRequest = onHidePopup
    ) {
        val colors = LocalAndroidColorScheme.current
        Row(
            modifier =
                Modifier.height(56.dp)
                    .background(colors.secondary, RoundedCornerShape(50.dp))
                    .padding(16.dp),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Icon(
                imageVector = Icons.Outlined.TouchApp,
                contentDescription = stringResource(R.string.popup_on_dismiss_cta_tile_text),
                tint = colors.onSecondary,
                modifier = Modifier.size(20.dp)
            )
            Spacer(modifier = Modifier.size(8.dp))
            Text(
                text = stringResource(R.string.popup_on_dismiss_cta_tile_text),
                style = MaterialTheme.typography.titleSmall,
                color = colors.onSecondary,
            )
        }
    }
}

@Composable
private fun filledButtonColors(): ButtonColors {
    val colors = LocalAndroidColorScheme.current
+59 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.communal.ui.compose

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.motionEventSpy
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel

@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable
fun CommunalTouchableSurface(
    viewModel: CommunalViewModel,
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit,
) {

    val interactionSource = remember { MutableInteractionSource() }

    Box(
        modifier =
            modifier
                .combinedClickable(
                    onLongClick = viewModel::onLongClick,
                    onClick = viewModel::onClick,
                    interactionSource = interactionSource,
                    indication = null,
                )
                .onPreviewKeyEvent {
                    viewModel.signalUserInteraction()
                    false
                }
                .motionEventSpy { viewModel.signalUserInteraction() }
    ) {
        content()
    }
}
+192 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.communal.ui.compose.section

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.PopupType
import com.android.systemui.res.R
import javax.inject.Inject

class CommunalPopupSection
@Inject
constructor(
    private val viewModel: CommunalViewModel,
) {

    @Composable
    fun Popup() {
        val currentPopup by viewModel.currentPopup.collectAsStateWithLifecycle(initialValue = null)

        if (currentPopup == PopupType.CtaTile) {
            PopupOnDismissCtaTile(viewModel::onHidePopup)
        }

        AnimatedVisibility(
            visible = currentPopup == PopupType.CustomizeWidgetButton,
            modifier = Modifier.fillMaxSize()
        ) {
            ButtonToEditWidgets(
                onClick = {
                    viewModel.onHidePopup()
                    viewModel.onOpenWidgetEditor()
                },
                onDismissRequest = {
                    viewModel.onHidePopup()
                    viewModel.setSelectedKey(null)
                }
            )
        }
    }

    @Composable
    private fun AnimatedVisibilityScope.ButtonToEditWidgets(
        onClick: () -> Unit,
        onDismissRequest: () -> Unit,
    ) {
        Popup(
            alignment = Alignment.TopCenter,
            offset = IntOffset(0, 40),
            onDismissRequest = onDismissRequest,
        ) {
            val colors = LocalAndroidColorScheme.current
            Button(
                modifier =
                    Modifier.height(56.dp)
                        .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) }
                        .animateEnterExit(
                            enter =
                                fadeIn(
                                    initialAlpha = 0f,
                                    animationSpec =
                                        tween(durationMillis = 83, easing = LinearEasing)
                                ),
                            exit =
                                fadeOut(
                                    animationSpec =
                                        tween(
                                            durationMillis = 83,
                                            delayMillis = 167,
                                            easing = LinearEasing
                                        )
                                )
                        )
                        .background(colors.secondary, RoundedCornerShape(50.dp)),
                onClick = onClick,
            ) {
                Row(
                    modifier =
                        Modifier.animateEnterExit(
                            enter =
                                fadeIn(
                                    animationSpec =
                                        tween(
                                            durationMillis = 167,
                                            delayMillis = 83,
                                            easing = LinearEasing
                                        )
                                ),
                            exit =
                                fadeOut(
                                    animationSpec =
                                        tween(durationMillis = 167, easing = LinearEasing)
                                )
                        )
                ) {
                    Icon(
                        imageVector = Icons.Outlined.Widgets,
                        contentDescription =
                            stringResource(R.string.button_to_configure_widgets_text),
                        tint = colors.onSecondary,
                        modifier = Modifier.size(20.dp)
                    )
                    Spacer(modifier = Modifier.size(8.dp))
                    Text(
                        text = stringResource(R.string.button_to_configure_widgets_text),
                        style = MaterialTheme.typography.titleSmall,
                        color = colors.onSecondary
                    )
                }
            }
        }
    }

    @Composable
    private fun PopupOnDismissCtaTile(onDismissRequest: () -> Unit) {
        Popup(
            alignment = Alignment.TopCenter,
            offset = IntOffset(0, 40),
            onDismissRequest = onDismissRequest
        ) {
            val colors = LocalAndroidColorScheme.current
            Row(
                modifier =
                    Modifier.height(56.dp)
                        .background(colors.secondary, RoundedCornerShape(50.dp))
                        .padding(16.dp),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Icon(
                    imageVector = Icons.Outlined.TouchApp,
                    contentDescription = stringResource(R.string.popup_on_dismiss_cta_tile_text),
                    tint = colors.onSecondary,
                    modifier = Modifier.size(20.dp)
                )
                Spacer(modifier = Modifier.size(8.dp))
                Text(
                    text = stringResource(R.string.popup_on_dismiss_cta_tile_text),
                    style = MaterialTheme.typography.titleSmall,
                    color = colors.onSecondary,
                )
            }
        }
    }
}
+6 −3
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
@@ -91,6 +92,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
@@ -154,13 +156,14 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
                context.resources,
                kosmos.keyguardTransitionInteractor,
                kosmos.keyguardInteractor,
                mock<KeyguardIndicationController>(),
                kosmos.communalSceneInteractor,
                kosmos.communalInteractor,
                kosmos.communalSettingsInteractor,
                kosmos.communalTutorialInteractor,
                kosmos.shadeInteractor,
                mediaHost,
                logcatLogBuffer("CommunalViewModelTest"),
                logcatLogBuffer("CommunalViewModelTest")
            )
    }

@@ -358,7 +361,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            val currentPopup by collectLastValue(underTest.currentPopup)

            assertThat(currentPopup).isNull()
            underTest.onShowCustomizeWidgetButton()
            underTest.onLongClick()
            assertThat(currentPopup).isEqualTo(PopupType.CustomizeWidgetButton)
            advanceTimeBy(POPUP_AUTO_HIDE_TIMEOUT_MS)
            assertThat(currentPopup).isNull()
@@ -370,7 +373,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
            val currentPopup by collectLastValue(underTest.currentPopup)

            underTest.onShowCustomizeWidgetButton()
            underTest.onLongClick()
            assertThat(currentPopup).isEqualTo(PopupType.CustomizeWidgetButton)

            underTest.onHidePopup()
Loading