diff options
11 files changed, 260 insertions, 18 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 46d418a03c00..37804682ed98 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,12 +30,9 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.transitions +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalViewModel - -object Scenes { - val Blank = SceneKey(name = "blank") - val Communal = SceneKey(name = "communal") -} +import kotlinx.coroutines.flow.transform object Communal { object Elements { @@ -43,7 +41,7 @@ object Communal { } val sceneTransitions = transitions { - from(Scenes.Blank, to = Scenes.Communal) { + from(TransitionSceneKey.Blank, to = TransitionSceneKey.Communal) { spec = tween(durationMillis = 500) translate(Communal.Elements.Content, Edge.Right) @@ -58,8 +56,14 @@ val sceneTransitions = transitions { * handling and transitions before the full Flexiglass layout is ready. */ @Composable -fun CommunalContainer(modifier: Modifier = Modifier, viewModel: CommunalViewModel) { - val (currentScene, setCurrentScene) = remember { mutableStateOf(Scenes.Blank) } +fun CommunalContainer( + modifier: Modifier = Modifier, + viewModel: CommunalViewModel, +) { + val currentScene: SceneKey by + viewModel.currentScene + .transform<CommunalSceneKey, SceneKey> { value -> value.toTransitionSceneKey() } + .collectAsState(TransitionSceneKey.Blank) // Failsafe to hide the whole SceneTransitionLayout in case of bugginess. var showSceneTransitionLayout by remember { mutableStateOf(true) } @@ -70,16 +74,19 @@ fun CommunalContainer(modifier: Modifier = Modifier, viewModel: CommunalViewMode SceneTransitionLayout( modifier = modifier.fillMaxSize(), currentScene = currentScene, - onChangeScene = setCurrentScene, + onChangeScene = { sceneKey -> viewModel.onSceneChanged(sceneKey.toCommunalSceneKey()) }, transitions = sceneTransitions, ) { - scene(Scenes.Blank, userActions = mapOf(Swipe.Left to Scenes.Communal)) { + scene( + TransitionSceneKey.Blank, + userActions = mapOf(Swipe.Left to TransitionSceneKey.Communal) + ) { BlankScene { showSceneTransitionLayout = false } } scene( - Scenes.Communal, - userActions = mapOf(Swipe.Right to Scenes.Blank), + TransitionSceneKey.Communal, + userActions = mapOf(Swipe.Right to TransitionSceneKey.Blank), ) { CommunalScene(viewModel, modifier = modifier) } @@ -121,3 +128,17 @@ private fun SceneScope.CommunalScene( ) { Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) } } + +// TODO(b/293899074): Remove these conversions once Compose can be used throughout SysUI. +object TransitionSceneKey { + val Blank = CommunalSceneKey.Blank.toTransitionSceneKey() + val Communal = CommunalSceneKey.Communal.toTransitionSceneKey() +} + +fun CommunalSceneKey.toTransitionSceneKey(): SceneKey { + return SceneKey(name = toString(), identity = this) +} + +fun SceneKey.toCommunalSceneKey(): CommunalSceneKey { + return this.identity as CommunalSceneKey +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 485e5122cfad..9cab17e9f6c4 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -1,15 +1,28 @@ package com.android.systemui.communal.data.repository import com.android.systemui.FeatureFlags +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow /** Encapsulates the state of communal mode. */ interface CommunalRepository { /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean + + /** + * Target scene as requested by the underlying [SceneTransitionLayout] or through + * [setDesiredScene]. + */ + val desiredScene: StateFlow<CommunalSceneKey> + + /** Updates the requested scene. */ + fun setDesiredScene(desiredScene: CommunalSceneKey) } @SysUISingleton @@ -23,4 +36,12 @@ constructor( get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && featureFlags.communalHub() + + private val _desiredScene: MutableStateFlow<CommunalSceneKey> = + MutableStateFlow(CommunalSceneKey.Blank) + override val desiredScene: StateFlow<CommunalSceneKey> = _desiredScene.asStateFlow() + + override fun setDesiredScene(desiredScene: CommunalSceneKey) { + _desiredScene.value = desiredScene + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 62387079ab35..ccccbb67c6c0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -19,10 +19,13 @@ package com.android.systemui.communal.domain.interactor import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map /** Encapsulates business-logic related to communal mode. */ @SysUISingleton @@ -47,4 +50,22 @@ constructor( * (have an allocated id). */ val widgetContent: Flow<List<CommunalWidgetContentModel>> = widgetRepository.communalWidgets + + /** + * Target scene as requested by the underlying [SceneTransitionLayout] or through + * [onSceneChanged]. + */ + val desiredScene: StateFlow<CommunalSceneKey> = communalRepository.desiredScene + + /** + * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the + * [CommunalSceneKey.Communal]. + */ + val isCommunalShowing: Flow<Boolean> = + communalRepository.desiredScene.map { it == CommunalSceneKey.Communal } + + /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ + fun onSceneChanged(newScene: CommunalSceneKey) { + communalRepository.setDesiredScene(newScene) + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt new file mode 100644 index 000000000000..2be909c8e6d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 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.shared.model + +/** Definition of the possible scenes for the communal UI. */ +sealed class CommunalSceneKey( + private val loggingName: String, +) { + /** The communal scene containing the hub UI. */ + object Communal : CommunalSceneKey("communal") + + /** The default scene, shows nothing and is only there to allow swiping to communal. */ + object Blank : CommunalSceneKey("blank") + + override fun toString(): String { + return loggingName + } +} 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 390b580bad28..de9b56364c24 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 @@ -20,11 +20,13 @@ import android.appwidget.AppWidgetHost import android.content.Context import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @SysUISingleton @@ -33,7 +35,7 @@ class CommunalViewModel constructor( @Application private val context: Context, private val appWidgetHost: AppWidgetHost, - communalInteractor: CommunalInteractor, + private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, ) { /** Whether communal hub should show tutorial content. */ @@ -54,4 +56,9 @@ constructor( ) } } + + val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + fun onSceneChanged(scene: CommunalSceneKey) { + communalInteractor.onSceneChanged(scene) + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index f3d41aaf2221..955dbbf06122 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -35,7 +35,9 @@ import android.view.ViewGroupOverlay import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardViewController +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle @@ -57,6 +59,8 @@ import com.android.systemui.tracing.traceSection import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch private val TAG: String = MediaHierarchyManager::class.java.simpleName @@ -96,11 +100,13 @@ constructor( private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, + private val communalInteractor: CommunalInteractor, configurationController: ConfigurationController, wakefulnessLifecycle: WakefulnessLifecycle, panelEventsEvents: ShadeStateEvents, private val secureSettings: SecureSettings, @Main private val handler: Handler, + @Application private val coroutineScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, private val logger: MediaViewLogger, ) { @@ -209,7 +215,7 @@ constructor( else result.setIntersect(animationStartClipping, targetClipping) } - private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1) + private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_COMMUNAL_HUB + 1) /** * The last location where this view was at before going to the desired location. This is useful * for guided transitions. @@ -401,6 +407,9 @@ constructor( } } + /** Is the communal UI showing */ + private var isCommunalShowing: Boolean = false + /** * The current cross fade progress. 0.5f means it's just switching between the start and the end * location and the content is fully faded, while 0.75f means that we're halfway faded in again @@ -563,6 +572,14 @@ constructor( settingsObserver, UserHandle.USER_ALL ) + + // Listen to the communal UI state. + coroutineScope.launch { + communalInteractor.isCommunalShowing.collect { value -> + isCommunalShowing = value + updateDesiredLocation(forceNoAnimation = true) + } + } } private fun updateConfiguration() { @@ -1115,6 +1132,9 @@ constructor( qsExpansion > 0.4f && onLockscreen -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS + // TODO(b/308813166): revisit logic once interactions between the hub and + // shade/keyguard state are finalized + isCommunalShowing && communalInteractor.isCommunalEnabled -> LOCATION_COMMUNAL_HUB onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN else -> LOCATION_QQS } @@ -1224,6 +1244,9 @@ constructor( /** Attached on the dream overlay */ const val LOCATION_DREAM_OVERLAY = 3 + /** Attached to a view in the communal UI grid */ + const val LOCATION_COMMUNAL_HUB = 4 + /** Attached at the root of the hierarchy in an overlay */ const val IN_OVERLAY = -1000 @@ -1261,7 +1284,8 @@ private annotation class TransformationType MediaHierarchyManager.LOCATION_QS, MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN, - MediaHierarchyManager.LOCATION_DREAM_OVERLAY + MediaHierarchyManager.LOCATION_DREAM_OVERLAY, + MediaHierarchyManager.LOCATION_COMMUNAL_HUB ] ) @Retention(AnnotationRetention.SOURCE) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index 20ea60fa3a5f..16a703a6bfdd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -20,11 +20,11 @@ import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceIdSequence import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaLocation +import com.android.systemui.res.R import java.lang.IllegalArgumentException import javax.inject.Inject @@ -154,6 +154,8 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM + MediaHierarchyManager.LOCATION_COMMUNAL_HUB -> + MediaUiEvent.MEDIA_CAROUSEL_LOCATION_COMMUNAL else -> throw IllegalArgumentException("Unknown media carousel location $location") } logger.log(event) @@ -276,6 +278,8 @@ enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039), @UiEvent(doc = "The media carousel moved to the dream state") MEDIA_CAROUSEL_LOCATION_DREAM(1040), + @UiEvent(doc = "The media carousel moved to the communal hub UI") + MEDIA_CAROUSEL_LOCATION_COMMUNAL(1520), @UiEvent(doc = "A media recommendation card was added to the media carousel") MEDIA_RECOMMENDATION_ADDED(1041), @UiEvent(doc = "A media recommendation card was removed from the media carousel") diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 888cd0bf8b9a..8f752e59e806 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -46,6 +46,7 @@ public interface MediaModule { String QUICK_QS_PANEL = "media_quick_qs_panel"; String KEYGUARD = "media_keyguard"; String DREAM = "dream"; + String COMMUNAL_HUB = "communal_Hub"; /** */ @Provides @@ -87,6 +88,16 @@ public interface MediaModule { return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); } + /** */ + @Provides + @SysUISingleton + @Named(COMMUNAL_HUB) + static MediaHost providesCommunalMediaHost(MediaHost.MediaHostStateHolder stateHolder, + MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, + MediaHostStatesManager statesManager) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + } + /** Provides a logging buffer related to the media tap-to-transfer chip on the sender device. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 8e21f294a361..2f17b6fa35ed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.coroutines.collectLastValue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -86,4 +87,48 @@ class CommunalInteractorTest : SysuiTestCase() { val interactor = CommunalInteractor(communalRepository, widgetRepository) assertThat(interactor.isCommunalEnabled).isFalse() } + + @Test + fun listensToSceneChange() = + testScope.runTest { + val interactor = CommunalInteractor(communalRepository, widgetRepository) + var desiredScene = collectLastValue(interactor.desiredScene) + runCurrent() + assertThat(desiredScene()).isEqualTo(CommunalSceneKey.Blank) + + val targetScene = CommunalSceneKey.Communal + communalRepository.setDesiredScene(targetScene) + desiredScene = collectLastValue(interactor.desiredScene) + runCurrent() + assertThat(desiredScene()).isEqualTo(targetScene) + } + + @Test + fun updatesScene() = + testScope.runTest { + val interactor = CommunalInteractor(communalRepository, widgetRepository) + val targetScene = CommunalSceneKey.Communal + + interactor.onSceneChanged(targetScene) + + val desiredScene = collectLastValue(communalRepository.desiredScene) + runCurrent() + assertThat(desiredScene()).isEqualTo(targetScene) + } + + @Test + fun isCommunalShowing() = + testScope.runTest { + val interactor = CommunalInteractor(communalRepository, widgetRepository) + + var isCommunalShowing = collectLastValue(interactor.isCommunalShowing) + runCurrent() + assertThat(isCommunalShowing()).isEqualTo(false) + + interactor.onSceneChanged(CommunalSceneKey.Communal) + + isCommunalShowing = collectLastValue(interactor.isCommunalShowing) + runCurrent() + assertThat(isCommunalShowing()).isEqualTo(true) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 5296f1abd756..5bfe56931bb4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -25,6 +25,10 @@ import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle @@ -46,6 +50,11 @@ import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.FakeSettings import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Rule @@ -63,6 +72,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @@ -71,6 +81,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var lockHost: MediaHost @Mock private lateinit var qsHost: MediaHost @Mock private lateinit var qqsHost: MediaHost + @Mock private lateinit var hubModeHost: MediaHost @Mock private lateinit var bypassController: KeyguardBypassController @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @@ -93,10 +104,15 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() + private val communalRepository = FakeCommunalRepository(isCommunalEnabled = true) + private val communalInteractor = + CommunalInteractor(communalRepository, FakeCommunalWidgetRepository()) private val notifPanelEvents = ShadeExpansionStateManager() private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setup() { @@ -117,11 +133,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() { mediaDataManager, keyguardViewController, dreamOverlayStateController, + communalInteractor, configurationController, wakefulnessLifecycle, notifPanelEvents, settings, fakeHandler, + testScope.backgroundScope, ResourcesSplitShadeStateController(), logger, ) @@ -131,6 +149,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP) setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) + setupHost(hubModeHost, MediaHierarchyManager.LOCATION_COMMUNAL_HUB, COMMUNAL_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) whenever(mediaCarouselController.mediaCarouselScrollHandler) @@ -475,6 +494,33 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test + fun testCommunalLocation() = + testScope.runTest { + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + runCurrent() + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), + nullable(), + eq(false), + anyLong(), + anyLong() + ) + clearInvocations(mediaCarouselController) + + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + runCurrent() + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_QQS), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong() + ) + } + + @Test fun testQsExpandedChanged_noQqsMedia() { // When we are looking at QQS with active media whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) @@ -538,5 +584,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private const val QQS_TOP = 123 private const val QS_TOP = 456 private const val LOCKSCREEN_TOP = 789 + private const val COMMUNAL_TOP = 111 } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index e1c6ddefc13b..799bb403a8ff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -1,8 +1,17 @@ package com.android.systemui.communal.data.repository +import com.android.systemui.communal.shared.model.CommunalSceneKey +import kotlinx.coroutines.flow.MutableStateFlow + /** Fake implementation of [CommunalRepository]. */ -class FakeCommunalRepository : CommunalRepository { - override var isCommunalEnabled = false +class FakeCommunalRepository( + override var isCommunalEnabled: Boolean = false, + override val desiredScene: MutableStateFlow<CommunalSceneKey> = + MutableStateFlow(CommunalSceneKey.Blank) +) : CommunalRepository { + override fun setDesiredScene(desiredScene: CommunalSceneKey) { + this.desiredScene.value = desiredScene + } fun setIsCommunalEnabled(value: Boolean) { isCommunalEnabled = value |