Merge "Merge "Update midi services owner to avoid singleton" into main am: c668fc9e58 am: c6c5cfca28 am: c6ad19e744" into main
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 46d418a..3780468 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.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.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 @@
 }
 
 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 @@
  * 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 @@
     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 @@
 ) {
     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 485e512..9cab17e 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 @@
         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 6238707..ccccbb6 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 @@
 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 @@
      * (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 0000000..2be909c
--- /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 390b580..de9b563 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.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 @@
 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 @@
                 )
             }
         }
+
+    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 d9ff36f..b1ff708 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
@@ -36,7 +36,9 @@
 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.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.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 @@
     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 @@
         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 @@
             }
         }
 
+    /** 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 @@
             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 @@
                 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 @@
         /** 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 @@
             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 20ea60f..16a703a 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.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 @@
                     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 @@
     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 888cd0b..8f752e5 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 @@
     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 @@
         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 8e21f29..2f17b6f 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.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 @@
             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 5296f1a..5bfe569 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 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.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.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
@@ -71,6 +81,7 @@
     @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 @@
     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 @@
                 mediaDataManager,
                 keyguardViewController,
                 dreamOverlayStateController,
+                communalInteractor,
                 configurationController,
                 wakefulnessLifecycle,
                 notifPanelEvents,
                 settings,
                 fakeHandler,
+                testScope.backgroundScope,
                 ResourcesSplitShadeStateController(),
                 logger,
             )
@@ -131,6 +149,7 @@
         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 @@
     }
 
     @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 @@
         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 e1c6dde..799bb40 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