diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 82d943796e2a8666c51e56b3cb10262a02a0b076..a02c5ce8bb14af32b16af1bcd3eae3ad357204b8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -23,6 +23,8 @@ import com.android.systemui.communal.data.repository.CommunalRepositoryModule import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule +import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel +import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModelImpl import com.android.systemui.communal.widgets.CommunalWidgetModule import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.EditWidgetsActivityStarterImpl @@ -47,4 +49,9 @@ interface CommunalModule { fun bindEditWidgetsActivityStarter( starter: EditWidgetsActivityStarterImpl ): EditWidgetsActivityStarter + + @Binds + fun bindCommunalTransitionViewModel( + impl: CommunalTransitionViewModelImpl + ): CommunalTransitionViewModel } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..eed0aa397b65d14c49cdd9c94f857b21e7b05248 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToDreamingTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.merge + +/** View model for transitions related to the communal hub. */ +interface CommunalTransitionViewModel { + val isUmoOnCommunal: Flow<Boolean> +} + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class CommunalTransitionViewModelImpl +@Inject +constructor( + glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, + dreamToGlanceableHubTransitionViewModel: DreamingToGlanceableHubTransitionViewModel, + glanceableHubToDreamTransitionViewModel: GlanceableHubToDreamingTransitionViewModel, +) : CommunalTransitionViewModel { + /** + * Whether UMO location should be on communal. This flow is responsive to transitions so that a + * new value is emitted at the right step of a transition to/from communal hub that the location + * of UMO should be updated. + */ + override val isUmoOnCommunal: Flow<Boolean> = + merge( + lockscreenToGlanceableHubTransitionViewModel.showUmo, + glanceableHubToLockscreenTransitionViewModel.showUmo, + dreamToGlanceableHubTransitionViewModel.showUmo, + glanceableHubToDreamTransitionViewModel.showUmo, + ) + .distinctUntilChanged() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt index c64f277b519a4338497feef1ccfdc726ff38b8da..5cb2ec9642b9c1c7bfc05ffc3dcc3d2bc8de6908 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGlanceableHubTransitionViewModel.kt @@ -28,6 +28,7 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @@ -65,6 +66,15 @@ constructor( name = "DREAMING->GLANCEABLE_HUB: dreamOverlayAlpha", ) + // Show UMO once the transition starts. + val showUmo: Flow<Boolean> = + transitionAnimation + .sharedFlow( + duration = TO_GLANCEABLE_HUB_DURATION, + onStep = { it }, + ) + .map { step -> step != 0f } + private companion object { val TO_GLANCEABLE_HUB_DURATION = 1.seconds } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt index 478c4faa1be303fd7fa1b09a1afc5c5d9cf17e3f..90be4f90410544249636a8a082c4c50e25599907 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToDreamingTransitionViewModel.kt @@ -28,6 +28,7 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @@ -66,6 +67,15 @@ constructor( ) } + // Show UMO until transition finishes. + val showUmo: Flow<Boolean> = + transitionAnimation + .sharedFlow( + duration = FROM_GLANCEABLE_HUB_DURATION, + onStep = { it }, + ) + .map { step -> step != 1f } + private companion object { val FROM_GLANCEABLE_HUB_DURATION = 1.seconds } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index e5b596419efe34cce2fb79c6bffff402df88bbc6..f81598f717b07749a9744d8a51d0795096e52f89 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -64,6 +64,9 @@ constructor( ) .onStart { emit(0f) } + // Show UMO as long as keyguard is not visible. + val showUmo: Flow<Boolean> = keyguardAlpha.map { alpha -> alpha == 0f } + val keyguardTranslationX: Flow<StateToValue> = configurationInteractor .dimensionPixelSize(R.dimen.hub_to_lockscreen_transition_lockscreen_translation_x) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index 978e71e2a825e759fac1931db2c9426645bae041..0c33e18d0113afd947c4ab267423f28c63ba35f8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -63,6 +63,9 @@ constructor( ) .onStart { emit(1f) } + // Show UMO as long as keyguard is not visible. + val showUmo: Flow<Boolean> = keyguardAlpha.map { alpha -> alpha == 0f } + val keyguardTranslationX: Flow<StateToValue> = configurationInteractor .dimensionPixelSize(R.dimen.lockscreen_to_hub_transition_lockscreen_translation_x) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index dbd71f3e3a041d4296333a28583558db37bcfa1e..a4f3e2174791f061096ce208536b6079cb731188 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -36,7 +36,7 @@ import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators import com.android.app.tracing.traceSection import com.android.keyguard.KeyguardViewController -import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -58,7 +58,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.animation.UniqueObjectHostView -import com.android.systemui.util.kotlin.BooleanFlowOperators.and import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -102,7 +101,7 @@ constructor( private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, - private val communalInteractor: CommunalInteractor, + communalTransitionViewModel: CommunalTransitionViewModel, configurationController: ConfigurationController, wakefulnessLifecycle: WakefulnessLifecycle, shadeInteractor: ShadeInteractor, @@ -587,11 +586,10 @@ constructor( // Listen to the communal UI state. Make sure that communal UI is showing and hub itself is // available, ie. not disabled and able to be shown. coroutineScope.launch { - and(communalInteractor.isCommunalShowing, communalInteractor.isCommunalAvailable) - .collect { value -> - isCommunalShowing = value - updateDesiredLocation() - } + communalTransitionViewModel.isUmoOnCommunal.collect { value -> + isCommunalShowing = value + updateDesiredLocation(forceNoAnimation = true) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt index 45f49f01a43ea8a0af0fd31f09a39a6019d292b1..fa28036f274bdc36bbfb08bc6077c407d35e94ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt @@ -24,11 +24,8 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.domain.interactor.communalInteractor -import com.android.systemui.communal.domain.interactor.setCommunalAvailable -import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.ui.viewmodel.fakeCommunalTransitionViewModel import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle @@ -115,10 +112,10 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() - private val communalInteractor = kosmos.communalInteractor private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler + private var communalTransitionViewModel = kosmos.fakeCommunalTransitionViewModel @Before fun setup() { @@ -142,7 +139,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { mediaDataManager, keyguardViewController, dreamOverlayStateController, - communalInteractor, + communalTransitionViewModel, configurationController, wakefulnessLifecycle, shadeInteractor, @@ -510,11 +507,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Test fun testCommunalLocation() = testScope.runTest { - mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB) - kosmos.setCommunalAvailable(true) - runCurrent() - - communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + communalTransitionViewModel.setIsUmoOnCommunal(true) runCurrent() verify(mediaCarouselController) .onDesiredLocationChanged( @@ -526,7 +519,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { ) clearInvocations(mediaCarouselController) - communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + communalTransitionViewModel.setIsUmoOnCommunal(false) runCurrent() verify(mediaCarouselController) .onDesiredLocationChanged( @@ -541,15 +534,11 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Test fun testCommunalLocation_showsOverLockscreen() = testScope.runTest { - mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB) - kosmos.setCommunalAvailable(true) - runCurrent() - // Device is on lock screen. whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) // UMO goes to communal even over the lock screen. - communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + communalTransitionViewModel.setIsUmoOnCommunal(true) runCurrent() verify(mediaCarouselController) .onDesiredLocationChanged( @@ -564,14 +553,10 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Test fun testCommunalLocation_showsUntilQsExpands() = testScope.runTest { - mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB) - kosmos.setCommunalAvailable(true) - runCurrent() - // Device is on lock screen. whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) - communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + communalTransitionViewModel.setIsUmoOnCommunal(true) runCurrent() verify(mediaCarouselController) .onDesiredLocationChanged( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt new file mode 100644 index 0000000000000000000000000000000000000000..eaa657b76c3d5fb76c5c6de2082b37fdb4f8e65a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.communalTransitionViewModel by + Kosmos.Fixture<CommunalTransitionViewModel> { fakeCommunalTransitionViewModel } + +val Kosmos.fakeCommunalTransitionViewModel by Kosmos.Fixture { FakeCommunalTransitionViewModel() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/FakeCommunalTransitionViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/FakeCommunalTransitionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..409cc1d03fda316fa36ffd3e44ba833d427d93aa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/FakeCommunalTransitionViewModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeCommunalTransitionViewModel : CommunalTransitionViewModel { + private val _isUmoOnCommunal = MutableStateFlow(false) + override val isUmoOnCommunal: Flow<Boolean> = _isUmoOnCommunal + + fun setIsUmoOnCommunal(value: Boolean) { + _isUmoOnCommunal.value = value + } +}