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

Commit b3b91b41 authored by Coco Duan's avatar Coco Duan
Browse files

Show an empty state when all widgets have been removed

Show a full size CTA for users to add widgets when there is
no widget available in GH.
Added an option in EditWidgetsActivity to automatically open
the widget picker upon entering the edit mode.

Bug: b/330175529
Test: atest CommunalViewModelTest
Test: atest CommunalInteractorTest
Test: manually remove all widgets and verify the flow
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: Iacf4b0f6e8d2580a07265663f40efd3f406d441b
parent 426f4938
Loading
Loading
Loading
Loading
+93 −23
Original line number Diff line number Diff line
@@ -111,6 +111,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.core.view.setPadding
import androidx.window.layout.WindowMetricsCalculator
import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
@@ -155,6 +157,7 @@ fun CommunalHub(
        derivedStateOf { selectedKey.value != null || reorderingWidgets }
    }
    var isButtonToEditWidgetsShowing by remember { mutableStateOf(false) }
    val isEmptyState by viewModel.isEmptyState.collectAsState(initial = false)

    val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
    val contentOffset = beforeContentPadding(contentPadding).toOffset()
@@ -177,7 +180,7 @@ fun CommunalHub(
                        viewModel.setSelectedKey(key)
                    }
                }
                .thenIf(!viewModel.isEditMode) {
                .thenIf(!viewModel.isEditMode && !isEmptyState) {
                    Modifier.pointerInput(
                            gridState,
                            contentOffset,
@@ -218,6 +221,12 @@ fun CommunalHub(
                        .motionEventSpy { onMotionEvent(viewModel) }
                },
    ) {
        if (!viewModel.isEditMode && isEmptyState) {
            EmptyStateCta(
                contentPadding = contentPadding,
                viewModel = viewModel,
            )
        } else {
            CommunalHubLazyGrid(
                communalContent = communalContent,
                viewModel = viewModel,
@@ -238,6 +247,7 @@ fun CommunalHub(
                selectedKey = selectedKey,
                widgetConfigurator = widgetConfigurator,
            )
        }

        // TODO(b/326060686): Remove this once keyguard indication area can persist over hub
        if (viewModel is CommunalViewModel) {
@@ -457,6 +467,67 @@ private fun BoxScope.CommunalHubLazyGrid(
    }
}

/**
 * The empty state displays a fullscreen call-to-action (CTA) tile when no widgets are available.
 */
@Composable
private fun EmptyStateCta(
    contentPadding: PaddingValues,
    viewModel: BaseCommunalViewModel,
) {
    val colors = LocalAndroidColorScheme.current
    Card(
        modifier = Modifier.height(Dimensions.GridHeight).padding(contentPadding),
        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
        border = BorderStroke(3.dp, colors.primaryFixedDim),
        shape = RoundedCornerShape(size = 80.dp)
    ) {
        Column(
            modifier = Modifier.fillMaxSize().padding(horizontal = 110.dp),
            verticalArrangement =
                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                text = stringResource(R.string.title_for_empty_state_cta),
                style = MaterialTheme.typography.displaySmall,
                textAlign = TextAlign.Center,
                color = colors.secondaryFixed,
            )
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center,
            ) {
                Button(
                    modifier = Modifier.height(56.dp),
                    colors =
                        ButtonDefaults.buttonColors(
                            containerColor = colors.primaryFixed,
                            contentColor = colors.onPrimaryFixed,
                        ),
                    onClick = {
                        viewModel.onOpenWidgetEditor(
                            shouldOpenWidgetPickerOnStart = true,
                        )
                    },
                ) {
                    Icon(
                        imageVector = Icons.Default.Add,
                        contentDescription =
                            stringResource(R.string.label_for_button_in_empty_state_cta),
                        modifier = Modifier.size(24.dp)
                    )
                    Spacer(Modifier.width(ButtonDefaults.IconSpacing))
                    Text(
                        text = stringResource(R.string.label_for_button_in_empty_state_cta),
                        style = MaterialTheme.typography.titleSmall,
                    )
                }
            }
        }
    }
}

@Composable
private fun LockStateIcon(
    isUnlocked: Boolean,
@@ -511,7 +582,7 @@ private fun Toolbar(
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        val spacerModifier = Modifier.width(Dimensions.ToolbarButtonSpaceBetween)
        val spacerModifier = Modifier.width(ButtonDefaults.IconSpacing)
        Button(
            onClick = onOpenWidgetPicker,
            colors = filledButtonColors(),
@@ -1042,7 +1113,6 @@ object Dimensions {
    val ToolbarPaddingHorizontal = 16.dp
    val ToolbarButtonPaddingHorizontal = 24.dp
    val ToolbarButtonPaddingVertical = 16.dp
    val ToolbarButtonSpaceBetween = 8.dp
    val ButtonPadding =
        PaddingValues(
            vertical = ToolbarButtonPaddingVertical,
+7 −0
Original line number Diff line number Diff line
@@ -816,6 +816,13 @@ class CommunalInteractorTest : SysuiTestCase() {
            verify(editWidgetsActivityStarter).startActivity(widgetKey)
        }

    @Test
    fun showWidgetEditor_openWidgetPickerOnStart_startsActivity() =
        testScope.runTest {
            underTest.showWidgetEditor(shouldOpenWidgetPickerOnStart = true)
            verify(editWidgetsActivityStarter).startActivity(shouldOpenWidgetPickerOnStart = true)
        }

    @Test
    fun navigateToCommunalWidgetSettings_startsActivity() =
        testScope.runTest {
+35 −0
Original line number Diff line number Diff line
@@ -193,6 +193,41 @@ class CommunalViewModelTest : SysuiTestCase() {
                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
        }

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

            widgetRepository.setCommunalWidgets(emptyList())
            // UMO playing
            mediaRepository.mediaActive()
            smartspaceRepository.setCommunalSmartspaceTargets(emptyList())

            val isEmptyState by collectLastValue(underTest.isEmptyState)
            assertThat(isEmptyState).isTrue()
        }

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

            widgetRepository.setCommunalWidgets(
                listOf(
                    CommunalWidgetContentModel(
                        appWidgetId = 1,
                        priority = 1,
                        providerInfo = providerInfo,
                    )
                ),
            )
            mediaRepository.mediaInactive()
            smartspaceRepository.setCommunalSmartspaceTargets(emptyList())

            val isEmptyState by collectLastValue(underTest.isEmptyState)
            assertThat(isEmptyState).isFalse()
        }

    @Test
    fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() =
        testScope.runTest {
+9 −5
Original line number Diff line number Diff line
@@ -1117,15 +1117,15 @@
    <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
    <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>

    <!-- Text for CTA button that launches the hub mode widget editor on click. [CHAR LIMIT=50] -->
    <!-- Text for call-to-action 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] -->
    <!-- Text for call-to-action 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] -->
    <!-- Label for call-to-action 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] -->
    <!-- Label for call-to-action 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>
    <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] -->
    <!-- Text for the popup to be displayed after dismissing the call-to-action tile. [CHAR LIMIT=50] -->
    <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string>
    <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] -->
    <string name="button_to_configure_widgets_text">Customize widgets</string>
@@ -1139,6 +1139,10 @@
    <string name="hub_mode_add_widget_button_text">Add widget</string>
    <!-- Text for the button that exits the hub mode editing mode. [CHAR LIMIT=50] -->
    <string name="hub_mode_editing_exit_button_text">Done</string>
    <!-- Label for the button in the empty state call-to-action tile that will open the widget picker. [CHAR LIMIT=NONE] -->
    <string name="label_for_button_in_empty_state_cta">Add widgets</string>
    <!-- Title for the empty state call-to-action when no widgets are available in the hub. [CHAR LIMIT=NONE] -->
    <string name="title_for_empty_state_cta">Get quick access to your favorite app widgets without unlocking your tablet.</string>
    <!-- Title for the dialog that redirects users to change allowed widget category in settings. [CHAR LIMIT=NONE] -->
    <string name="dialog_title_to_allow_any_widget">Allow any widget on lock screen?</string>
    <!-- Text for the button in the dialog that opens when tapping on disabled widgets. [CHAR LIMIT=NONE] -->
+5 −2
Original line number Diff line number Diff line
@@ -264,8 +264,11 @@ constructor(
    }

    /** Show the widget editor Activity. */
    fun showWidgetEditor(preselectedKey: String? = null) {
        editWidgetsActivityStarter.startActivity(preselectedKey)
    fun showWidgetEditor(
        preselectedKey: String? = null,
        shouldOpenWidgetPickerOnStart: Boolean = false,
    ) {
        editWidgetsActivityStarter.startActivity(preselectedKey, shouldOpenWidgetPickerOnStart)
    }

    /**
Loading