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

Commit 46a6de1c authored by Coco Duan's avatar Coco Duan Committed by cocod
Browse files

Prevent the hub from recomposing while opening a timer activity

Create a flow `isCommunalContentFlowFrozen` to pause the emission of
communalContent flow. The freeze only happens if we last transitioned
to hub, keyguard is occluded and isAbleToDream is false as a new activity
is about to show on top.

Bug: b/338052219
Flag: com.android.systemui.communal_hub
Test: manually add a timer via voice and the grid doesn't re-flow
Test: atest CommunalViewModelTest
Change-Id: Ic0bea11e848da6820e44404005397b161bd88a7e
parent 8c255121
Loading
Loading
Loading
Loading
+230 −1
Original line number Diff line number Diff line
@@ -54,6 +54,8 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
@@ -62,6 +64,8 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.shade.ShadeTestUtil
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -71,7 +75,6 @@ import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
@@ -85,6 +88,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@@ -138,6 +142,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
        )
        whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))

        kosmos.powerInteractor.setAwakeForTest()

        underTest =
            CommunalViewModel(
                testScope,
@@ -468,6 +474,229 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
            assertThat(isFocusable).isEqualTo(false)
        }

    @Test
    fun isCommunalContentFlowFrozen_whenActivityStartedWhileDreaming() =
        testScope.runTest {
            val isCommunalContentFlowFrozen by
                collectLastValue(underTest.isCommunalContentFlowFrozen)

            // 1. When dreaming not dozing
            keyguardRepository.setDozeTransitionModel(
                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
            )
            keyguardRepository.setDreaming(true)
            keyguardRepository.setDreamingWithOverlay(true)
            advanceTimeBy(60L)
            // And keyguard is occluded by dream
            keyguardRepository.setKeyguardOccluded(true)

            // And on hub
            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.DREAMING,
                to = KeyguardState.GLANCEABLE_HUB,
                testScope = testScope,
            )

            // Then flow is not frozen
            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)

            // 2. When dreaming stopped by the new activity about to show on lock screen
            keyguardRepository.setDreamingWithOverlay(false)
            advanceTimeBy(60L)

            // Then flow is frozen
            assertThat(isCommunalContentFlowFrozen).isEqualTo(true)

            // 3. When transitioned to OCCLUDED and activity shows
            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.GLANCEABLE_HUB,
                to = KeyguardState.OCCLUDED,
                testScope = testScope,
            )

            // Then flow is not frozen
            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)
        }

    @Test
    fun isCommunalContentFlowFrozen_whenActivityStartedInHandheldMode() =
        testScope.runTest {
            val isCommunalContentFlowFrozen by
                collectLastValue(underTest.isCommunalContentFlowFrozen)

            // 1. When on keyguard and not occluded
            keyguardRepository.setKeyguardShowing(true)
            keyguardRepository.setKeyguardOccluded(false)

            // And transitioned to hub
            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.LOCKSCREEN,
                to = KeyguardState.GLANCEABLE_HUB,
                testScope = testScope,
            )

            // Then flow is not frozen
            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)

            // 2. When occluded by a new activity
            keyguardRepository.setKeyguardOccluded(true)
            runCurrent()

            // And transitioning to occluded
            keyguardTransitionRepository.sendTransitionStep(
                TransitionStep(
                    from = KeyguardState.GLANCEABLE_HUB,
                    to = KeyguardState.OCCLUDED,
                    transitionState = TransitionState.STARTED,
                )
            )

            keyguardTransitionRepository.sendTransitionStep(
                from = KeyguardState.GLANCEABLE_HUB,
                to = KeyguardState.OCCLUDED,
                transitionState = TransitionState.RUNNING,
                value = 0.5f,
            )

            // Then flow is frozen
            assertThat(isCommunalContentFlowFrozen).isEqualTo(true)

            // 3. When transition is finished
            keyguardTransitionRepository.sendTransitionStep(
                from = KeyguardState.GLANCEABLE_HUB,
                to = KeyguardState.OCCLUDED,
                transitionState = TransitionState.FINISHED,
                value = 1f,
            )

            // Then flow is not frozen
            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)
        }

    @Test
    fun communalContent_emitsFrozenContent_whenFrozen() =
        testScope.runTest {
            val communalContent by collectLastValue(underTest.communalContent)
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)

            // When dreaming
            keyguardRepository.setDozeTransitionModel(
                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
            )
            keyguardRepository.setDreaming(true)
            keyguardRepository.setDreamingWithOverlay(true)
            advanceTimeBy(60L)
            keyguardRepository.setKeyguardOccluded(true)

            // And transitioned to hub
            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.DREAMING,
                to = KeyguardState.GLANCEABLE_HUB,
                testScope = testScope,
            )

            // Widgets available
            val widgets =
                listOf(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = providerInfo,
                    ),
                )
            widgetRepository.setCommunalWidgets(widgets)

            // Then hub shows widgets and the CTA tile
            assertThat(communalContent).hasSize(3)

            // When dreaming stopped by another activity which should freeze flow
            keyguardRepository.setDreamingWithOverlay(false)
            advanceTimeBy(60L)

            // New timer available
            val target = Mockito.mock(SmartspaceTarget::class.java)
            whenever<String?>(target.smartspaceTargetId).thenReturn("target")
            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
            runCurrent()

            // Still only emits widgets and the CTA tile
            assertThat(communalContent).hasSize(3)
            assertThat(communalContent?.get(0))
                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
            assertThat(communalContent?.get(1))
                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
            assertThat(communalContent?.get(2))
                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
        }

    @Test
    fun communalContent_emitsLatestContent_whenNotFrozen() =
        testScope.runTest {
            val communalContent by collectLastValue(underTest.communalContent)
            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)

            // When dreaming
            keyguardRepository.setDozeTransitionModel(
                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
            )
            keyguardRepository.setDreaming(true)
            keyguardRepository.setDreamingWithOverlay(true)
            advanceTimeBy(60L)
            keyguardRepository.setKeyguardOccluded(true)

            // Transitioned to Glanceable hub.
            keyguardTransitionRepository.sendTransitionSteps(
                from = KeyguardState.DREAMING,
                to = KeyguardState.GLANCEABLE_HUB,
                testScope = testScope,
            )

            // And widgets available
            val widgets =
                listOf(
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 0,
                        priority = 30,
                        providerInfo = providerInfo,
                    ),
                    CommunalWidgetContentModel.Available(
                        appWidgetId = 1,
                        priority = 20,
                        providerInfo = providerInfo,
                    ),
                )
            widgetRepository.setCommunalWidgets(widgets)

            // Then emits widgets and the CTA tile
            assertThat(communalContent).hasSize(3)

            // When new timer available
            val target = Mockito.mock(SmartspaceTarget::class.java)
            whenever(target.smartspaceTargetId).thenReturn("target")
            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
            runCurrent()

            // Then emits timer, widgets and the CTA tile
            assertThat(communalContent).hasSize(4)
            assertThat(communalContent?.get(0))
                .isInstanceOf(CommunalContentModel.Smartspace::class.java)
            assertThat(communalContent?.get(1))
                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
            assertThat(communalContent?.get(2))
                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
            assertThat(communalContent?.get(3))
                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
        }

    private suspend fun setIsMainUser(isMainUser: Boolean) {
        whenever(user.isMain).thenReturn(isMainUser)
        userRepository.setUserInfos(listOf(user))
+6 −0
Original line number Diff line number Diff line
@@ -91,6 +91,12 @@ abstract class BaseCommunalViewModel(
    /** A list of all the communal content to be displayed in the communal hub. */
    abstract val communalContent: Flow<List<CommunalContentModel>>

    /**
     * Whether to freeze the emission of the communalContent flow to prevent recomposition. Defaults
     * to false, indicating that the flow will emit new update.
     */
    open val isCommunalContentFlowFrozen: Flow<Boolean> = flowOf(false)

    /** Whether in edit mode for the communal hub. */
    open val isEditMode = false

+37 −1
Original line number Diff line number Diff line
@@ -38,7 +38,9 @@ import com.android.systemui.media.controls.ui.view.MediaHostState
import com.android.systemui.media.dagger.MediaModule
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
@@ -76,8 +78,11 @@ constructor(

    private val logger = Logger(logBuffer, "CommunalViewModel")

    /** Communal content saved from the previous emission when the flow is active (not "frozen"). */
    private var frozenCommunalContent: List<CommunalContentModel>? = null

    @OptIn(ExperimentalCoroutinesApi::class)
    override val communalContent: Flow<List<CommunalContentModel>> =
    private val latestCommunalContent: Flow<List<CommunalContentModel>> =
        tutorialInteractor.isTutorialAvailable
            .flatMapLatest { isTutorialMode ->
                if (isTutorialMode) {
@@ -93,9 +98,40 @@ constructor(
                }
            }
            .onEach { models ->
                frozenCommunalContent = models
                logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
            }

    /**
     * Freeze the content flow, when an activity is about to show, like starting a timer via voice:
     * 1) in handheld mode, use the keyguard occluded state;
     * 2) in dreaming mode, where keyguard is already occluded by dream, use the dream wakeup
     *    signal. Since in this case the shell transition info does not include
     *    KEYGUARD_VISIBILITY_TRANSIT_FLAGS, KeyguardTransitionHandler will not run the
     *    occludeAnimation on KeyguardViewMediator.
     */
    override val isCommunalContentFlowFrozen: Flow<Boolean> =
        allOf(
                keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB),
                keyguardInteractor.isKeyguardOccluded,
                not(keyguardInteractor.isAbleToDream)
            )
            .distinctUntilChanged()
            .onEach { logger.d("isCommunalContentFlowFrozen: $it") }

    override val communalContent: Flow<List<CommunalContentModel>> =
        isCommunalContentFlowFrozen
            .flatMapLatestConflated { isFrozen ->
                if (isFrozen) {
                    flowOf(frozenCommunalContent ?: emptyList())
                } else {
                    latestCommunalContent
                }
            }
            .onEach { models ->
                logger.d({ "CommunalContent: $str1" }) { str1 = models.joinToString { it.key } }
            }

    override val isEmptyState: Flow<Boolean> =
        communalInteractor.widgetContent
            .map { it.isEmpty() }