diff options
| author | 2024-05-09 17:46:52 +0000 | |
|---|---|---|
| committer | 2024-05-31 05:53:59 +0000 | |
| commit | 46a6de1c9260b1f3debaa24f5080b0b7c40c6060 (patch) | |
| tree | 66bb5e4e130e7655e5131ccfa88e270c5c0f58d4 | |
| parent | 8c2551219f4e40fa0267a5c09ebe8730243ea01b (diff) | |
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
3 files changed, 273 insertions, 2 deletions
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 5e19a41f345c..a1bad392799d 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 @@ -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)) 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 3d9e8615fb18..8cd5603bdc7f 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 @@ -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 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 7f3a2dcb23dc..ce69ee880ce1 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 @@ -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() } |