diff options
| author | 2024-01-10 02:32:27 +0000 | |
|---|---|---|
| committer | 2024-01-10 02:32:27 +0000 | |
| commit | 1609fc2887fecd4e93aa2a34b95b007a28ec3958 (patch) | |
| tree | 99452db48d0661533ba94d91a7ee8981e5bce1d8 | |
| parent | e799141c5775987f368d0664e760a26065e5d9c6 (diff) | |
| parent | b55426b63c12f954b4a6648fc3e7ee82ad1a3c2b (diff) | |
Merge "Show popup message after dismissing the CTA tile" into main
5 files changed, 141 insertions, 2 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index d3d8e4ed3523..d76f0ff3ec18 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -45,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.TouchApp import androidx.compose.material.icons.outlined.Widgets import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors @@ -76,10 +77,12 @@ 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.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Popup import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize @@ -97,6 +100,8 @@ fun CommunalHub( onEditDone: (() -> Unit)? = null, ) { val communalContent by viewModel.communalContent.collectAsState(initial = emptyList()) + val isPopupOnDismissCtaShowing by + viewModel.isPopupOnDismissCtaShowing.collectAsState(initial = false) var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var toolbarSize: IntSize? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } @@ -133,6 +138,10 @@ fun CommunalHub( } } + if (isPopupOnDismissCtaShowing) { + PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta) + } + // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge // swipe back to the blank scene. @@ -319,8 +328,40 @@ private fun Toolbar( } @Composable +private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) { + Popup( + alignment = Alignment.TopCenter, + offset = IntOffset(0, 40), + onDismissRequest = onHidePopupAfterDismissCta + ) { + 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 RemoveButtonContent(spacerModifier: Modifier) { - Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_open_widget_editor)) + Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_remove_widget)) Spacer(spacerModifier) Text( text = stringResource(R.string.button_to_remove_widget), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 16e0bc00ad35..8e3f66464de6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository @@ -41,7 +42,9 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -50,6 +53,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class CommunalViewModelTest : SysuiTestCase() { @@ -84,6 +88,7 @@ class CommunalViewModelTest : SysuiTestCase() { underTest = CommunalViewModel( + testScope, withDeps.communalInteractor, WidgetInteractionHandler(mock()), withDeps.tutorialInteractor, @@ -159,4 +164,44 @@ class CommunalViewModelTest : SysuiTestCase() { assertThat(communalContent?.get(4)) .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java) } + + @Test + fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + communalRepository.setCtaTileInViewModeVisibility(true) + + val communalContent by collectLastValue(underTest.communalContent) + val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) + + assertThat(communalContent?.size).isEqualTo(1) + assertThat(communalContent?.get(0)) + .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java) + + underTest.onDismissCtaTile() + + // hide CTA tile and show the popup + assertThat(communalContent).isEmpty() + assertThat(isPopupOnDismissCtaShowing).isEqualTo(true) + + // hide popup after time elapsed + advanceTimeBy(POPUP_AUTO_HIDE_TIMEOUT_MS) + assertThat(isPopupOnDismissCtaShowing).isEqualTo(false) + } + + @Test + fun popup_onDismiss_hidesImmediately() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + communalRepository.setCtaTileInViewModeVisibility(true) + + val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) + + underTest.onDismissCtaTile() + assertThat(isPopupOnDismissCtaShowing).isEqualTo(true) + + // dismiss the popup directly + underTest.onHidePopupAfterDismissCta() + assertThat(isPopupOnDismissCtaShowing).isEqualTo(false) + } } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3f11faebffb1..5e109053c994 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1087,6 +1087,8 @@ <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> + <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] --> + <string name="popup_on_dismiss_cta_tile_text">Long press to customize 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] --> diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 4cb83a3b51dc..28f48ce1e647 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -30,6 +30,7 @@ import com.android.systemui.shade.ShadeViewController import javax.inject.Provider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf /** The base view model for the communal hub. */ abstract class BaseCommunalViewModel( @@ -96,6 +97,12 @@ abstract class BaseCommunalViewModel( /** Whether in edit mode for the communal hub. */ open val isEditMode = false + /** Whether the popup message triggered by dismissing the CTA tile is showing. */ + open val isPopupOnDismissCtaShowing: Flow<Boolean> = flowOf(false) + + /** Hide the popup message triggered by dismissing the CTA tile. */ + open fun onHidePopupAfterDismissCta() {} + /** Called as the UI requests deleting a widget. */ open fun onDeleteWidget(id: Int) {} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 066e897cdfdb..1c696851bb70 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -23,23 +23,31 @@ import com.android.systemui.communal.domain.interactor.CommunalTutorialInteracto import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import com.android.systemui.shade.ShadeViewController import javax.inject.Inject import javax.inject.Named import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch /** The default view model used for showing the communal hub. */ @SysUISingleton class CommunalViewModel @Inject constructor( + @Application private val scope: CoroutineScope, private val communalInteractor: CommunalInteractor, private val interactionHandler: WidgetInteractionHandler, tutorialInteractor: CommunalTutorialInteractor, @@ -63,9 +71,45 @@ constructor( } } + private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isPopupOnDismissCtaShowing: Flow<Boolean> = + _isPopupOnDismissCtaShowing.asStateFlow() + override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() - override fun onDismissCtaTile() = communalInteractor.dismissCtaTile() + override fun onDismissCtaTile() { + communalInteractor.dismissCtaTile() + setPopupOnDismissCtaVisibility(true) + schedulePopupHiding() + } override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler + + override fun onHidePopupAfterDismissCta() { + cancelDelayedPopupHiding() + setPopupOnDismissCtaVisibility(false) + } + + private fun setPopupOnDismissCtaVisibility(isVisible: Boolean) { + _isPopupOnDismissCtaShowing.value = isVisible + } + + private var delayedHidePopupJob: Job? = null + private fun schedulePopupHiding() { + cancelDelayedPopupHiding() + delayedHidePopupJob = + scope.launch { + delay(POPUP_AUTO_HIDE_TIMEOUT_MS) + onHidePopupAfterDismissCta() + } + } + + private fun cancelDelayedPopupHiding() { + delayedHidePopupJob?.cancel() + delayedHidePopupJob = null + } + + companion object { + const val POPUP_AUTO_HIDE_TIMEOUT_MS = 12000L + } } |