Creating home control panel dream service

Test: atest HomeControlsDreamStartableTest, HomeControlsComponentInteractorTest, HomeControlsDreamServiceTest
Flag: ACONFIG FLAG_HOME_PANEL_DREAM DEVELOPMENT
Change-Id: I97e2d551e56380c9c86d96f8f64e49e9dea7a9d3
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 54ab5d1..0050676 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1083,5 +1083,25 @@
         <!-- Allow SystemUI to listen for the capabilities defined in the linked xml -->
         <property android:name="android.net.PROPERTY_SELF_CERTIFIED_CAPABILITIES"
                   android:value="@xml/self_certified_network_capabilities_both" />
+
+
+        <service
+            android:name="com.android.systemui.dreams.homecontrols.HomeControlsDreamService"
+            android:exported="false"
+            android:enabled="false"
+            android:label="@string/home_controls_dream_label"
+            android:description="@string/home_controls_dream_description"
+            android:permission="android.permission.BIND_DREAM_SERVICE"
+            android:icon="@drawable/controls_icon"
+            >
+
+            <intent-filter>
+                <action android:name="android.service.dreams.DreamService" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data
+                android:name="android.service.dream"
+                android:resource="@xml/home_controls_dream_metadata" />
+        </service>
     </application>
 </manifest>
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
new file mode 100644
index 0000000..efccf7a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.dreams.homecontrols
+
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.homeControlsComponentInteractor by
+    Kosmos.Fixture {
+        HomeControlsComponentInteractor(
+            selectedComponentRepository = selectedComponentRepository,
+            controlsComponent,
+            authorizedPanelsRepository = authorizedPanelsRepository,
+            userRepository = fakeUserRepository,
+            bgScope = applicationCoroutineScope,
+        )
+    }
+
+val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() }
+val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() }
+val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
new file mode 100644
index 0000000..ce74a90
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.FakeSelectedComponentRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+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.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsComponentInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    private lateinit var underTest: HomeControlsComponentInteractor
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var selectedComponentRepository: FakeSelectedComponentRepository
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        userRepository = kosmos.fakeUserRepository
+        userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER))
+
+        controlsComponent = kosmos.controlsComponent
+        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+        controlsListingController = kosmos.controlsListingController
+        selectedComponentRepository = kosmos.selectedComponentRepository
+
+        selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle)
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        underTest =
+            HomeControlsComponentInteractor(
+                selectedComponentRepository,
+                controlsComponent,
+                authorizedPanelsRepository,
+                userRepository,
+                kosmos.applicationCoroutineScope,
+            )
+    }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameForSelectedItemByUser() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                whenever(controlsListingController.getCurrentServices())
+                    .thenReturn(
+                        listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))
+                    )
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate(false)
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf())
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsComponentNameForDifferentUsers() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                userRepository.setSelectedUserInfo(ANOTHER_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+                selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+                runServicesUpdate()
+                assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL)
+            }
+        }
+
+    @Test
+    fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(authorizedPanelsRepository.getAuthorizedPanels())
+                    .thenReturn(setOf(TEST_PACKAGE_PANEL))
+                whenever(controlsComponent.getControlsListingController())
+                    .thenReturn(Optional.empty())
+                userRepository.setSelectedUserInfo(PRIMARY_USER)
+                selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+                val actualValue by collectLastValue(underTest.panelComponent)
+                assertThat(actualValue).isNull()
+            }
+        }
+
+    private fun runServicesUpdate(hasPanelBoolean: Boolean = true) {
+        val listings =
+            listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean))
+        val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) }
+        callback.onServicesUpdated(listings)
+    }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+
+        private const val ANOTHER_USER_ID = 1
+        private val ANOTHER_USER =
+            UserInfo(
+                /* id= */ ANOTHER_USER_ID,
+                /* name= */ "another user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+        private const val TEST_PACKAGE = "pkg"
+        private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service")
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                true
+            )
+        private val TEST_SELECTED_COMPONENT_NON_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                false
+            )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
new file mode 100644
index 0000000..d28b6bf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.settings.FakeControlsSettingsRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamServiceTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository
+    @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
+    @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
+    @Mock private lateinit var activity: Activity
+    private val logBuffer: LogBuffer = create()
+
+    private lateinit var underTest: HomeControlsDreamService
+    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+    private lateinit var fakeDreamActivityProvider: DreamActivityProvider
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
+            .thenReturn(taskFragmentComponent)
+
+        controlsSettingsRepository = FakeControlsSettingsRepository()
+        controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true)
+
+        controlsComponent = kosmos.controlsComponent
+        controlsListingController = kosmos.controlsListingController
+
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+        fakeDreamActivityProvider = DreamActivityProvider { activity }
+        underTest =
+            HomeControlsDreamService(
+                controlsSettingsRepository,
+                taskFragmentComponentFactory,
+                homeControlsComponentInteractor,
+                fakeDreamActivityProvider,
+                logBuffer
+            )
+    }
+
+    @Test
+    fun testOnAttachedToWindowCreatesTaskFragmentComponent() {
+        underTest.onAttachedToWindow()
+        verify(taskFragmentComponentFactory).create(any(), any(), any(), any())
+    }
+
+    @Test
+    fun testOnDetachedFromWindowDestroyTaskFragmentComponent() {
+        underTest.onAttachedToWindow()
+        underTest.onDetachedFromWindow()
+        verify(taskFragmentComponent).destroy()
+    }
+
+    @Test
+    fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() {
+        fakeDreamActivityProvider = DreamActivityProvider { null }
+        underTest =
+            HomeControlsDreamService(
+                controlsSettingsRepository,
+                taskFragmentComponentFactory,
+                homeControlsComponentInteractor,
+                fakeDreamActivityProvider,
+                logBuffer
+            )
+
+        underTest.onAttachedToWindow()
+        verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
+    }
+
+    companion object {
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
new file mode 100644
index 0000000..6610e70
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt
@@ -0,0 +1,202 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.controls.panels.selectedComponentRepository
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+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.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import java.util.Optional
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsDreamStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    @Mock private lateinit var packageManager: PackageManager
+
+    private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor
+    private lateinit var selectedComponentRepository: SelectedComponentRepository
+    private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository
+    private lateinit var userRepository: FakeUserRepository
+    private lateinit var controlsComponent: ControlsComponent
+    private lateinit var controlsListingController: ControlsListingController
+
+    private lateinit var startable: HomeControlsDreamStartable
+    private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+    private val testScope = kosmos.testScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        selectedComponentRepository = kosmos.selectedComponentRepository
+        authorizedPanelsRepository = kosmos.authorizedPanelsRepository
+        userRepository = kosmos.fakeUserRepository
+        controlsComponent = kosmos.controlsComponent
+        controlsListingController = kosmos.controlsListingController
+
+        userRepository.setUserInfos(listOf(PRIMARY_USER))
+
+        whenever(authorizedPanelsRepository.getAuthorizedPanels())
+            .thenReturn(setOf(TEST_PACKAGE_PANEL))
+
+        whenever(controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+        whenever(controlsListingController.getCurrentServices())
+            .thenReturn(listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)))
+
+        homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor
+
+        startable =
+            HomeControlsDreamStartable(
+                mContext,
+                packageManager,
+                homeControlsComponentInteractor,
+                kosmos.applicationCoroutineScope
+            )
+    }
+
+    @Test
+    @EnableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartEnablesHomeControlsDreamServiceWhenPanelComponentIsNotNull() =
+        testScope.runTest {
+            userRepository.setSelectedUserInfo(PRIMARY_USER)
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager)
+                .setComponentEnabledSetting(
+                    eq(componentName),
+                    eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                    eq(PackageManager.DONT_KILL_APP)
+                )
+        }
+
+    @Test
+    @EnableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartDisablesHomeControlsDreamServiceWhenPanelComponentIsNull() =
+        testScope.runTest {
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager)
+                .setComponentEnabledSetting(
+                    eq(componentName),
+                    eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                    eq(PackageManager.DONT_KILL_APP)
+                )
+        }
+
+    @Test
+    @DisableFlags(FLAG_HOME_PANEL_DREAM)
+    fun testStartDoesNotRunDreamServiceWhenFlagIsDisabled() =
+        testScope.runTest {
+            selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL)
+            startable.start()
+            runCurrent()
+            verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any())
+        }
+
+    private fun ControlsServiceInfo(
+        componentName: ComponentName,
+        label: CharSequence,
+        hasPanel: Boolean
+    ): ControlsServiceInfo {
+        val serviceInfo =
+            ServiceInfo().apply {
+                applicationInfo = ApplicationInfo()
+                packageName = componentName.packageName
+                name = componentName.className
+            }
+        return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel)
+    }
+
+    private class FakeControlsServiceInfo(
+        context: Context,
+        serviceInfo: ServiceInfo,
+        private val label: CharSequence,
+        hasPanel: Boolean
+    ) : ControlsServiceInfo(context, serviceInfo) {
+
+        init {
+            if (hasPanel) {
+                panelActivity = serviceInfo.componentName
+            }
+        }
+
+        override fun loadLabel(): CharSequence {
+            return label
+        }
+    }
+
+    companion object {
+        private const val PRIMARY_USER_ID = 0
+        private val PRIMARY_USER =
+            UserInfo(
+                /* id= */ PRIMARY_USER_ID,
+                /* name= */ "primary user",
+                /* flags= */ UserInfo.FLAG_PRIMARY
+            )
+        private const val TEST_PACKAGE_PANEL = "pkg.panel"
+        private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service")
+        private val TEST_SELECTED_COMPONENT_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                true
+            )
+        private val TEST_SELECTED_COMPONENT_NON_PANEL =
+            SelectedComponentRepository.SelectedComponent(
+                TEST_PACKAGE_PANEL,
+                TEST_COMPONENT_PANEL,
+                false
+            )
+    }
+}
diff --git a/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
new file mode 100644
index 0000000..00b461b
--- /dev/null
+++ b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png
Binary files differ
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 17719d1..7a83070 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -991,7 +991,7 @@
 
     <!-- Component name for Home Panel Dream -->
     <string name="config_homePanelDreamComponent" translatable="false">
-        @null
+        com.android.systemui/com.android.systemui.dreams.homecontrols.HomeControlsDreamService
     </string>
 
     <!--
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7943588..8971859 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3314,4 +3314,8 @@
     <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
     <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
     <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
+    <!-- Label for home control panel [CHAR LIMIT=30] -->
+    <string name="home_controls_dream_label">Home Controls</string>
+    <!-- Description for home control panel [CHAR LIMIT=50] -->
+    <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string>
 </resources>
diff --git a/packages/SystemUI/res/xml/home_controls_dream_metadata.xml b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
new file mode 100644
index 0000000..eb7c79e
--- /dev/null
+++ b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ 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.
+  -->
+<dream xmlns:android="http://schemas.android.com/apk/res/android"
+       android:showClockAndComplications="false"
+       android:previewImage="@drawable/homecontrols_sq"
+    />
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 95233f7..22719b0 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.dagger.qualifiers.PerUser
 import com.android.systemui.dreams.AssistantAttentionMonitor
 import com.android.systemui.dreams.DreamMonitor
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable
 import com.android.systemui.globalactions.GlobalActionsComponent
 import com.android.systemui.keyboard.KeyboardUI
 import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable
@@ -332,4 +333,9 @@
     abstract fun bindCommunalAppWidgetHostStartable(
         impl: CommunalAppWidgetHostStartable
     ): CoreStartable
+
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsDreamStartable::class)
+    abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 0656933..ba74742 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -17,6 +17,7 @@
 package com.android.systemui.dreams.dagger;
 
 import android.annotation.Nullable;
+import android.app.Service;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -30,12 +31,18 @@
 import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
 import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
+import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
+import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
+import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
 import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule;
 import com.android.systemui.res.R;
 import com.android.systemui.touch.TouchInsetManager;
 
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
 
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -88,6 +95,15 @@
     }
 
     /**
+     * Provides Home Controls Dream Service
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(HomeControlsDreamService.class)
+    Service bindHomeControlsDreamService(
+            HomeControlsDreamService service);
+
+    /**
      * Provides a touch inset manager for dreams.
      */
     @Provides
@@ -151,4 +167,9 @@
     static String providesDreamOverlayWindowTitle(@Main Resources resources) {
         return resources.getString(R.string.app_label);
     }
+
+    /** Provides activity for dream service */
+    @Binds
+    DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl);
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
new file mode 100644
index 0000000..b35b7f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+
+fun interface DreamActivityProvider {
+    /**
+     * Provides abstraction for getting the activity associated with a dream service, so that the
+     * activity can be mocked in tests.
+     */
+    fun getActivity(dreamService: DreamService): Activity?
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
new file mode 100644
index 0000000..0854e93
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+import javax.inject.Inject
+
+class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
+    override fun getActivity(dreamService: DreamService): Activity {
+        return dreamService.activity
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
new file mode 100644
index 0000000..e04a505
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.Intent
+import android.service.controls.ControlsProviderService
+import android.service.dreams.DreamService
+import android.window.TaskFragmentInfo
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import javax.inject.Inject
+
+class HomeControlsDreamService
+@Inject
+constructor(
+    private val controlsSettingsRepository: ControlsSettingsRepository,
+    private val taskFragmentFactory: TaskFragmentComponent.Factory,
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    private val dreamActivityProvider: DreamActivityProvider,
+    @DreamLog logBuffer: LogBuffer
+) : DreamService() {
+    private lateinit var taskFragmentComponent: TaskFragmentComponent
+
+    private val logger = DreamLogger(logBuffer, "HomeControlsDreamService")
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        val activity = dreamActivityProvider.getActivity(this)
+        if (activity == null) {
+            finish()
+            return
+        }
+        taskFragmentComponent =
+            taskFragmentFactory
+                .create(
+                    activity = activity,
+                    onCreateCallback = this::onTaskFragmentCreated,
+                    onInfoChangedCallback = this::onTaskFragmentInfoChanged,
+                    hide = { finish() }
+                )
+                .apply { createTaskFragment() }
+    }
+
+    private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) {
+        if (taskFragmentInfo.isEmpty) {
+            logger.d("Finishing dream due to TaskFragment being empty")
+            finish()
+        }
+    }
+
+    private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) {
+        val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
+        val componentName = homeControlsComponentInteractor.panelComponent.value
+        logger.d("Starting embedding $componentName")
+        val intent =
+            Intent().apply {
+                component = componentName
+                putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting)
+                putExtra(
+                    ControlsProviderService.EXTRA_CONTROLS_SURFACE,
+                    ControlsProviderService.CONTROLS_SURFACE_DREAM
+                )
+            }
+        taskFragmentComponent.startActivityInTaskFragment(intent)
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        taskFragmentComponent.destroy()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
new file mode 100644
index 0000000..6cd94c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.service.controls.flags.Flags.homePanelDream
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class HomeControlsDreamStartable
+@Inject
+constructor(
+    private val context: Context,
+    private val packageManager: PackageManager,
+    private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    @Background private val bgScope: CoroutineScope,
+) : CoreStartable {
+
+    private val componentName = ComponentName(context, HomeControlsDreamService::class.java)
+
+    override fun start() {
+        if (!homePanelDream()) return
+        bgScope.launch {
+            homeControlsComponentInteractor.panelComponent.collect { selectedPanelComponent ->
+                setEnableHomeControlPanel(selectedPanelComponent != null)
+            }
+        }
+    }
+
+    private fun setEnableHomeControlPanel(enabled: Boolean) {
+        val packageState =
+            if (enabled) {
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+            } else {
+                PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+            }
+        packageManager.setComponentEnabledSetting(
+            componentName,
+            packageState,
+            PackageManager.DONT_KILL_APP
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
new file mode 100644
index 0000000..6f7dcb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Binder
+import android.window.TaskFragmentCreationParams
+import android.window.TaskFragmentInfo
+import android.window.TaskFragmentOperation
+import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT
+import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK
+import android.window.TaskFragmentOrganizer
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE
+import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN
+import android.window.TaskFragmentTransaction
+import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED
+import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED
+import android.window.WindowContainerTransaction
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+typealias FragmentInfoCallback = (TaskFragmentInfo) -> Unit
+
+/** Wrapper around TaskFragmentOrganizer for managing a task fragment within an activity */
+class TaskFragmentComponent
+@AssistedInject
+constructor(
+    @Assisted private val activity: Activity,
+    @Assisted("onCreateCallback") private val onCreateCallback: FragmentInfoCallback,
+    @Assisted("onInfoChangedCallback") private val onInfoChangedCallback: FragmentInfoCallback,
+    @Assisted private val hide: () -> Unit,
+    @Main private val executor: DelayableExecutor,
+) {
+
+    @AssistedFactory
+    fun interface Factory {
+        fun create(
+            activity: Activity,
+            @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback,
+            @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback,
+            hide: () -> Unit
+        ): TaskFragmentComponent
+    }
+
+    private val fragmentToken = Binder()
+    private val organizer: TaskFragmentOrganizer =
+        object : TaskFragmentOrganizer(executor) {
+
+                override fun onTransactionReady(transaction: TaskFragmentTransaction) {
+                    handleTransactionReady(transaction)
+                }
+            }
+            .apply { registerOrganizer(true /* isSystemOrganizer */) }
+
+    private fun handleTransactionReady(transaction: TaskFragmentTransaction) {
+        val resultT = WindowContainerTransaction()
+
+        for (change in transaction.changes) {
+            change.taskFragmentInfo?.let { taskFragmentInfo ->
+                if (taskFragmentInfo.fragmentToken == fragmentToken) {
+                    when (change.type) {
+                        TYPE_TASK_FRAGMENT_APPEARED -> {
+                            resultT.addTaskFragmentOperation(
+                                fragmentToken,
+                                TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK)
+                                    .build()
+                            )
+
+                            onCreateCallback(taskFragmentInfo)
+                        }
+                        TYPE_TASK_FRAGMENT_INFO_CHANGED -> {
+                            onInfoChangedCallback(taskFragmentInfo)
+                        }
+                        TYPE_TASK_FRAGMENT_VANISHED -> {
+                            hide()
+                        }
+                        TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {}
+                        TYPE_TASK_FRAGMENT_ERROR -> {
+                            hide()
+                        }
+                        TYPE_ACTIVITY_REPARENTED_TO_TASK -> {}
+                        else ->
+                            throw IllegalArgumentException(
+                                "Unknown TaskFragmentEvent=" + change.type
+                            )
+                    }
+                }
+            }
+        }
+        organizer.onTransactionHandled(
+            transaction.transactionToken,
+            resultT,
+            TASK_FRAGMENT_TRANSIT_CHANGE,
+            false
+        )
+    }
+
+    /** Creates the task fragment */
+    fun createTaskFragment() {
+        val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds)
+        val fragmentOptions =
+            TaskFragmentCreationParams.Builder(
+                    organizer.organizerToken,
+                    fragmentToken,
+                    activity.activityToken!!
+                )
+                .setInitialRelativeBounds(taskBounds)
+                .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+                .build()
+        organizer.applyTransaction(
+            WindowContainerTransaction().createTaskFragment(fragmentOptions),
+            TASK_FRAGMENT_TRANSIT_CHANGE,
+            false
+        )
+    }
+
+    private fun WindowContainerTransaction.startActivity(intent: Intent) =
+        this.startActivityInTaskFragment(fragmentToken, activity.activityToken!!, intent, null)
+
+    /** Starts the provided activity in the fragment and move it to the background */
+    fun startActivityInTaskFragment(intent: Intent) {
+        organizer.applyTransaction(
+            WindowContainerTransaction().startActivity(intent),
+            TASK_FRAGMENT_TRANSIT_OPEN,
+            false
+        )
+    }
+
+    /** Destroys the task fragment */
+    fun destroy() {
+        organizer.applyTransaction(
+            WindowContainerTransaction()
+                .addTaskFragmentOperation(
+                    fragmentToken,
+                    TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build()
+                ),
+            TASK_FRAGMENT_TRANSIT_CLOSE,
+            false
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
new file mode 100644
index 0000000..91e0547
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.dreams.homecontrols.domain.interactor
+
+import android.content.ComponentName
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.panels.AuthorizedPanelsRepository
+import com.android.systemui.controls.panels.SelectedComponentRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.getOrNull
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class HomeControlsComponentInteractor
+@Inject
+constructor(
+    private val selectedComponentRepository: SelectedComponentRepository,
+    private val controlsComponent: ControlsComponent,
+    private val authorizedPanelsRepository: AuthorizedPanelsRepository,
+    userRepository: UserRepository,
+    @Background private val bgScope: CoroutineScope
+) {
+    private val controlsListingController =
+        controlsComponent.getControlsListingController().getOrNull()
+
+    /** Gets the current user's selected panel, or null if there isn't one */
+    private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> =
+        userRepository.selectedUserInfo
+            .flatMapLatest { user ->
+                selectedComponentRepository.selectedComponentFlow(user.userHandle)
+            }
+            .map { if (it?.isPanel == true) it else null }
+
+    /** Gets all the available panels which are authorized by the user */
+    private fun allPanelItem(): Flow<List<PanelComponent>> {
+        if (controlsListingController == null) {
+            return emptyFlow()
+        }
+        return conflatedCallbackFlow {
+                val listener =
+                    object : ControlsListingController.ControlsListingCallback {
+                        override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                            trySend(serviceInfos)
+                        }
+                    }
+                controlsListingController.addCallback(listener)
+                awaitClose { controlsListingController.removeCallback(listener) }
+            }
+            .onStart { emit(controlsListingController.getCurrentServices()) }
+            .map { serviceInfos ->
+                val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels()
+                serviceInfos.mapNotNull {
+                    val panelActivity = it.panelActivity
+                    if (it.componentName.packageName in authorizedPanels && panelActivity != null) {
+                        PanelComponent(it.componentName, panelActivity)
+                    } else {
+                        null
+                    }
+                }
+            }
+    }
+    val panelComponent: StateFlow<ComponentName?> =
+        combine(allPanelItem(), selectedItem) { items, selected ->
+                val item =
+                    items.firstOrNull { it.componentName == selected?.componentName }
+                        ?: items.firstOrNull()
+                item?.panelActivity
+            }
+            .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
+
+    data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
index 002862e..a231212 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.controls.panels
 
 import android.os.UserHandle
+import com.android.systemui.kosmos.Kosmos
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -68,3 +69,6 @@
         }
     }
 }
+
+val Kosmos.selectedComponentRepository by
+    Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt