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

Commit 705402ee authored by Coco Duan's avatar Coco Duan
Browse files

Add CTA tile to hub mode

In glanceable hub view mode, the CTA tile is used to customize hub mode
and can be dismissed on button click.
In glanceable hub edit mode, the CTA tile is used to add more widgets.
And the tile is not dismissable.
The tile will be displayed as the last card in the grid.

Bug: b/313462210
Flag: ACONFIG com.android.systemui.communal_hub DEVELOPMENT
Test: atest CommunalInteractorTest
Test: atest CommunalEditModeViewModelTest
Test: atest CommunalViewModelTest
Change-Id: I519f000ea25b338f896211c1932e4b1699a42669
parent 80b12388
Loading
Loading
Loading
Loading
+134 −12
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -44,6 +45,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
@@ -51,6 +53,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -71,6 +74,7 @@ import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
@@ -111,7 +115,8 @@ fun CommunalHub(
                isDraggingToRemove =
                    checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates)
                isDraggingToRemove
            }
            },
            onOpenWidgetPicker = onOpenWidgetPicker,
        )

        if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
@@ -148,13 +153,14 @@ private fun BoxScope.CommunalHubLazyGrid(
    contentPadding: PaddingValues,
    setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
    updateDragPositionForRemove: (offset: Offset) -> Boolean,
    onOpenWidgetPicker: (() -> Unit)? = null,
) {
    var gridModifier = Modifier.align(Alignment.CenterStart)
    val gridState = rememberLazyGridState()
    var list = communalContent
    var dragDropState: GridDragDropState? = null
    if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
        val contentListState = rememberContentListState(communalContent, viewModel)
        val contentListState = rememberContentListState(list, viewModel)
        list = contentListState.list
        // for drag & drop operations within the communal hub grid
        dragDropState =
@@ -207,7 +213,7 @@ private fun BoxScope.CommunalHubLazyGrid(
            if (viewModel.isEditMode && dragDropState != null) {
                DraggableItem(
                    dragDropState = dragDropState,
                    enabled = true,
                    enabled = list[index] is CommunalContentModel.Widget,
                    index = index,
                    size = size
                ) { _ ->
@@ -216,6 +222,7 @@ private fun BoxScope.CommunalHubLazyGrid(
                        model = list[index],
                        viewModel = viewModel,
                        size = size,
                        onOpenWidgetPicker = onOpenWidgetPicker,
                    )
                }
            } else {
@@ -256,16 +263,11 @@ private fun Toolbar(
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        val buttonContentPadding =
            PaddingValues(
                vertical = Dimensions.ToolbarButtonPaddingVertical,
                horizontal = Dimensions.ToolbarButtonPaddingHorizontal,
            )
        val spacerModifier = Modifier.width(Dimensions.ToolbarButtonSpaceBetween)
        Button(
            onClick = onOpenWidgetPicker,
            colors = filledButtonColors(),
            contentPadding = buttonContentPadding
            contentPadding = Dimensions.ButtonPadding
        ) {
            Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor))
            Spacer(spacerModifier)
@@ -285,7 +287,7 @@ private fun Toolbar(
                        disabledContainerColor = colors.primary,
                        disabledContentColor = colors.onPrimary,
                    ),
                contentPadding = buttonContentPadding,
                contentPadding = Dimensions.ButtonPadding,
                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
            ) {
                RemoveButtonContent(spacerModifier)
@@ -297,7 +299,7 @@ private fun Toolbar(
                onClick = {},
                colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary),
                border = BorderStroke(width = 1.0.dp, color = colors.primary),
                contentPadding = buttonContentPadding,
                contentPadding = Dimensions.ButtonPadding,
                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
            ) {
                RemoveButtonContent(spacerModifier)
@@ -307,7 +309,7 @@ private fun Toolbar(
        Button(
            onClick = onEditDone,
            colors = filledButtonColors(),
            contentPadding = buttonContentPadding
            contentPadding = Dimensions.ButtonPadding
        ) {
            Text(
                text = stringResource(R.string.hub_mode_editing_exit_button_text),
@@ -340,10 +342,15 @@ private fun CommunalContent(
    viewModel: BaseCommunalViewModel,
    size: SizeF,
    modifier: Modifier = Modifier,
    onOpenWidgetPicker: (() -> Unit)? = null,
) {
    when (model) {
        is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier)
        is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size)
        is CommunalContentModel.CtaTileInViewMode ->
            CtaTileInViewModeContent(viewModel, size, modifier)
        is CommunalContentModel.CtaTileInEditMode ->
            CtaTileInEditModeContent(size, modifier, onOpenWidgetPicker)
        is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
        is CommunalContentModel.Tutorial -> TutorialContent(modifier)
        is CommunalContentModel.Umo -> Umo(viewModel, modifier)
@@ -361,6 +368,115 @@ fun WidgetPlaceholderContent(size: SizeF) {
    ) {}
}

/** Presents a CTA tile at the end of the grid, to customize the hub. */
@Composable
private fun CtaTileInViewModeContent(
    viewModel: BaseCommunalViewModel,
    size: SizeF,
    modifier: Modifier = Modifier,
) {
    val colors = LocalAndroidColorScheme.current
    Card(
        modifier = modifier.height(size.height.dp),
        colors =
            CardDefaults.cardColors(
                containerColor = colors.primary,
                contentColor = colors.onPrimary,
            ),
        shape = RoundedCornerShape(80.dp, 40.dp, 80.dp, 40.dp)
    ) {
        Column(
            modifier = Modifier.fillMaxSize().padding(horizontal = 82.dp),
            verticalArrangement =
                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Icon(
                imageVector = Icons.Outlined.Widgets,
                contentDescription = stringResource(R.string.cta_label_to_open_widget_picker),
                modifier = Modifier.size(Dimensions.IconSize),
            )
            Text(
                text = stringResource(R.string.cta_label_to_edit_widget),
                style = MaterialTheme.typography.titleLarge,
                textAlign = TextAlign.Center,
            )
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center,
            ) {
                OutlinedButton(
                    colors =
                        ButtonDefaults.buttonColors(
                            contentColor = colors.onPrimary,
                        ),
                    border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
                    contentPadding = Dimensions.ButtonPadding,
                    onClick = viewModel::onDismissCtaTile,
                ) {
                    Text(
                        text = stringResource(R.string.cta_tile_button_to_dismiss),
                    )
                }
                Spacer(modifier = Modifier.size(Dimensions.Spacing))
                Button(
                    colors =
                        ButtonDefaults.buttonColors(
                            containerColor = colors.primaryContainer,
                            contentColor = colors.onPrimaryContainer,
                        ),
                    contentPadding = Dimensions.ButtonPadding,
                    onClick = viewModel::onOpenWidgetEditor
                ) {
                    Text(
                        text = stringResource(R.string.cta_tile_button_to_open_widget_editor),
                    )
                }
            }
        }
    }
}

/** Presents a CTA tile at the end of the hub in edit mode, to add more widgets. */
@Composable
private fun CtaTileInEditModeContent(
    size: SizeF,
    modifier: Modifier = Modifier,
    onOpenWidgetPicker: (() -> Unit)? = null,
) {
    if (onOpenWidgetPicker == null) {
        throw IllegalArgumentException("onOpenWidgetPicker should not be null.")
    }
    val colors = LocalAndroidColorScheme.current
    Card(
        modifier = modifier.height(size.height.dp),
        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
        border = BorderStroke(1.dp, colors.primary),
        shape = RoundedCornerShape(200.dp),
        onClick = onOpenWidgetPicker,
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement =
                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Icon(
                imageVector = Icons.Outlined.Widgets,
                contentDescription = stringResource(R.string.cta_label_to_open_widget_picker),
                tint = colors.primary,
                modifier = Modifier.size(Dimensions.IconSize),
            )
            Text(
                text = stringResource(R.string.cta_label_to_open_widget_picker),
                style = MaterialTheme.typography.titleLarge,
                color = colors.primary,
                textAlign = TextAlign.Center,
            )
        }
    }
}

@Composable
private fun WidgetContent(
    viewModel: BaseCommunalViewModel,
@@ -513,4 +629,10 @@ object Dimensions {
    val ToolbarButtonPaddingHorizontal = 24.dp
    val ToolbarButtonPaddingVertical = 16.dp
    val ToolbarButtonSpaceBetween = 8.dp
    val ButtonPadding =
        PaddingValues(
            vertical = ToolbarButtonPaddingVertical,
            horizontal = ToolbarButtonPaddingHorizontal,
        )
    val IconSize = 48.dp
}
+26 −0
Original line number Diff line number Diff line
@@ -326,6 +326,32 @@ class CommunalInteractorTest : SysuiTestCase() {
            assertThat(ongoingContent?.get(3)?.size).isEqualTo(CommunalContentSize.THIRD)
        }

    @Test
    fun cta_visibilityTrue_shows() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
            communalRepository.setCtaTileInViewModeVisibility(true)

            val ctaTileContent by collectLastValue(underTest.ctaTileContent)

            assertThat(ctaTileContent?.size).isEqualTo(1)
            assertThat(ctaTileContent?.get(0))
                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
            assertThat(ctaTileContent?.get(0)?.key)
                .isEqualTo(CommunalContentModel.KEY.CTA_TILE_IN_VIEW_MODE_KEY)
        }

    @Test
    fun ctaTile_visibilityFalse_doesNotShow() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
            communalRepository.setCtaTileInViewModeVisibility(false)

            val ctaTileContent by collectLastValue(underTest.ctaTileContent)

            assertThat(ctaTileContent).isEmpty()
        }

    @Test
    fun listensToSceneChange() =
        testScope.runTest {
+5 −3
Original line number Diff line number Diff line
@@ -91,7 +91,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
    }

    @Test
    fun communalContent_onlyWidgetsAreShownInEditMode() =
    fun communalContent_onlyWidgetsAndCtaTileAreShownInEditMode() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)

@@ -123,12 +123,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {

            val communalContent by collectLastValue(underTest.communalContent)

            // Only Widgets are shown.
            assertThat(communalContent?.size).isEqualTo(2)
            // Only Widgets and CTA tile are shown.
            assertThat(communalContent?.size).isEqualTo(3)
            assertThat(communalContent?.get(0))
                .isInstanceOf(CommunalContentModel.Widget::class.java)
            assertThat(communalContent?.get(1))
                .isInstanceOf(CommunalContentModel.Widget::class.java)
            assertThat(communalContent?.get(2))
                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
        }

    @Test
+8 −3
Original line number Diff line number Diff line
@@ -112,7 +112,7 @@ class CommunalViewModelTest : SysuiTestCase() {
        }

    @Test
    fun ordering_smartspaceBeforeUmoBeforeWidgets() =
    fun ordering_smartspaceBeforeUmoBeforeWidgetsBeforeCtaTile() =
        testScope.runTest {
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)

@@ -142,10 +142,13 @@ class CommunalViewModelTest : SysuiTestCase() {
            // Media playing.
            mediaRepository.mediaActive()

            // CTA Tile not dismissed.
            communalRepository.setCtaTileInViewModeVisibility(true)

            val communalContent by collectLastValue(underTest.communalContent)

            // Order is smart space, then UMO, then widget content.
            assertThat(communalContent?.size).isEqualTo(4)
            // Order is smart space, then UMO, widget content and cta tile.
            assertThat(communalContent?.size).isEqualTo(5)
            assertThat(communalContent?.get(0))
                .isInstanceOf(CommunalContentModel.Smartspace::class.java)
            assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java)
@@ -153,5 +156,7 @@ class CommunalViewModelTest : SysuiTestCase() {
                .isInstanceOf(CommunalContentModel.Widget::class.java)
            assertThat(communalContent?.get(3))
                .isInstanceOf(CommunalContentModel.Widget::class.java)
            assertThat(communalContent?.get(4))
                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
        }
}
+8 −0
Original line number Diff line number Diff line
@@ -1079,6 +1079,14 @@

    <!-- Description for the button that opens the widget editor on click. [CHAR LIMIT=50] -->
    <string name="button_to_open_widget_editor">Open the widget editor</string>
    <!-- Text for CTA button that launches the hub mode widget editor on click. [CHAR LIMIT=50] -->
    <string name="cta_tile_button_to_open_widget_editor">Customize</string>
    <!-- Text for CTA button that dismisses the tile on click. [CHAR LIMIT=50] -->
    <string name="cta_tile_button_to_dismiss">Dismiss</string>
    <!-- Label for CTA tile to edit the glanceable hub [CHAR LIMIT=100] -->
    <string name="cta_label_to_edit_widget">Add, remove, and reorder your widgets in this space</string>
    <!-- Label for CTA tile that opens widget picker on click in edit mode [CHAR LIMIT=50] -->
    <string name="cta_label_to_open_widget_picker">Add more widgets</string>
    <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
    <string name="button_to_remove_widget">Remove</string>
    <!-- Text for the button that launches the hub mode widget picker. [CHAR LIMIT=50] -->
Loading