Loading packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +93 −23 Original line number Diff line number Diff line Loading @@ -110,6 +110,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 Loading Loading @@ -154,6 +156,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() Loading @@ -178,7 +181,7 @@ fun CommunalHub( viewModel.setSelectedKey(key) } } .thenIf(!viewModel.isEditMode) { .thenIf(!viewModel.isEditMode && !isEmptyState) { Modifier.pointerInput( gridState, contentOffset, Loading Loading @@ -219,6 +222,12 @@ fun CommunalHub( .motionEventSpy { onMotionEvent(viewModel) } }, ) { if (!viewModel.isEditMode && isEmptyState) { EmptyStateCta( contentPadding = contentPadding, viewModel = viewModel, ) } else { CommunalHubLazyGrid( communalContent = communalContent, viewModel = viewModel, Loading @@ -239,6 +248,7 @@ fun CommunalHub( selectedKey = selectedKey, widgetConfigurator = widgetConfigurator, ) } // TODO(b/326060686): Remove this once keyguard indication area can persist over hub if (viewModel is CommunalViewModel) { Loading Loading @@ -460,6 +470,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, Loading Loading @@ -514,7 +585,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(), Loading Loading @@ -1004,7 +1075,6 @@ object Dimensions { val ToolbarPaddingHorizontal = 16.dp val ToolbarButtonPaddingHorizontal = 24.dp val ToolbarButtonPaddingVertical = 16.dp val ToolbarButtonSpaceBetween = 8.dp val ButtonPadding = PaddingValues( vertical = ToolbarButtonPaddingVertical, Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +35 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading packages/SystemUI/res/values/strings.xml +9 −5 Original line number Diff line number Diff line Loading @@ -1119,15 +1119,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> Loading @@ -1141,6 +1141,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] --> Loading packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +5 −2 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +93 −23 Original line number Diff line number Diff line Loading @@ -110,6 +110,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 Loading Loading @@ -154,6 +156,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() Loading @@ -178,7 +181,7 @@ fun CommunalHub( viewModel.setSelectedKey(key) } } .thenIf(!viewModel.isEditMode) { .thenIf(!viewModel.isEditMode && !isEmptyState) { Modifier.pointerInput( gridState, contentOffset, Loading Loading @@ -219,6 +222,12 @@ fun CommunalHub( .motionEventSpy { onMotionEvent(viewModel) } }, ) { if (!viewModel.isEditMode && isEmptyState) { EmptyStateCta( contentPadding = contentPadding, viewModel = viewModel, ) } else { CommunalHubLazyGrid( communalContent = communalContent, viewModel = viewModel, Loading @@ -239,6 +248,7 @@ fun CommunalHub( selectedKey = selectedKey, widgetConfigurator = widgetConfigurator, ) } // TODO(b/326060686): Remove this once keyguard indication area can persist over hub if (viewModel is CommunalViewModel) { Loading Loading @@ -460,6 +470,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, Loading Loading @@ -514,7 +585,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(), Loading Loading @@ -1004,7 +1075,6 @@ object Dimensions { val ToolbarPaddingHorizontal = 16.dp val ToolbarButtonPaddingHorizontal = 24.dp val ToolbarButtonPaddingVertical = 16.dp val ToolbarButtonSpaceBetween = 8.dp val ButtonPadding = PaddingValues( vertical = ToolbarButtonPaddingVertical, Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +35 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading
packages/SystemUI/res/values/strings.xml +9 −5 Original line number Diff line number Diff line Loading @@ -1119,15 +1119,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> Loading @@ -1141,6 +1141,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] --> Loading
packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +5 −2 Original line number Diff line number Diff line Loading @@ -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