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

Commit 1609fc28 authored by Coco Duan's avatar Coco Duan Committed by Android (Google) Code Review
Browse files

Merge "Show popup message after dismissing the CTA tile" into main

parents e799141c b55426b6
Loading
Loading
Loading
Loading
+42 −1
Original line number Diff line number Diff line
@@ -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.
@@ -318,9 +327,41 @@ 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),
+45 −0
Original line number Diff line number Diff line
@@ -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)
        }
}
+2 −0
Original line number Diff line number Diff line
@@ -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] -->
+7 −0
Original line number Diff line number Diff line
@@ -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) {}

+45 −1
Original line number Diff line number Diff line
@@ -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
    }
}