summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Coco Duan <cocod@google.com> 2024-05-09 17:46:52 +0000
committer cocod <cocod@google.com> 2024-05-31 05:53:59 +0000
commit46a6de1c9260b1f3debaa24f5080b0b7c40c6060 (patch)
tree66bb5e4e130e7655e5131ccfa88e270c5c0f58d4
parent8c2551219f4e40fa0267a5c09ebe8730243ea01b (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
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt231
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt38
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() }