Implement remote home controls data source

Implements a remote data source for the information home controls dreams
needs, which will query the remote service in SYSTEM_USER.

Flag: com.android.systemui.home_controls_dream_hsum
Bug: com.android.systemui.home_controls_dream_hsum
Test: atest HomeControlsRemoteProxyTest
Test: atest RemoteHomeControlsDataSourceDelegatorTest
Change-Id: I97c5b455debfce0499daf78b1510df946c771f60
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt
new file mode 100644
index 0000000..e57776f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.service
+
+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.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeControlsRemoteProxyTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val fakeBinder = kosmos.fakeHomeControlsRemoteBinder
+
+    private val underTest by lazy { kosmos.homeControlsRemoteProxy }
+
+    @Test
+    fun testRegistersOnlyWhileSubscribed() =
+        testScope.runTest {
+            assertThat(fakeBinder.callbacks).isEmpty()
+
+            val job = launch { underTest.componentInfo.collect {} }
+            runCurrent()
+            assertThat(fakeBinder.callbacks).hasSize(1)
+
+            job.cancel()
+            runCurrent()
+            assertThat(fakeBinder.callbacks).isEmpty()
+        }
+
+    @Test
+    fun testEmitsOnCallback() =
+        testScope.runTest {
+            val componentInfo by collectLastValue(underTest.componentInfo)
+            assertThat(componentInfo).isNull()
+
+            fakeBinder.notifyCallbacks(TEST_COMPONENT, allowTrivialControlsOnLockscreen = true)
+            assertThat(componentInfo)
+                .isEqualTo(
+                    HomeControlsComponentInfo(
+                        TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = true,
+                    )
+                )
+        }
+
+    @Test
+    fun testOnlyRegistersSingleCallbackForMultipleSubscribers() =
+        testScope.runTest {
+            assertThat(fakeBinder.callbacks).isEmpty()
+
+            // 2 collectors
+            val job = launch {
+                launch { underTest.componentInfo.collect {} }
+                launch { underTest.componentInfo.collect {} }
+            }
+            runCurrent()
+            assertThat(fakeBinder.callbacks).hasSize(1)
+            job.cancel()
+        }
+
+    private companion object {
+        val TEST_COMPONENT = ComponentName("pkg.test", "class.test")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt
new file mode 100644
index 0000000..4002175
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.service
+
+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.coroutines.collectLastValue
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.service.ObservableServiceConnection
+import com.android.systemui.util.service.PersistentConnectionManager
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+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.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RemoteHomeControlsDataSourceDelegatorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val proxy = kosmos.homeControlsRemoteProxy
+    private val fakeBinder = kosmos.fakeHomeControlsRemoteBinder
+
+    private val callbackCaptor =
+        argumentCaptor<ObservableServiceConnection.Callback<HomeControlsRemoteProxy>>()
+
+    private val connectionManager =
+        mock<PersistentConnectionManager<HomeControlsRemoteProxy>> {
+            on { start() } doAnswer { simulateConnect() }
+            on { stop() } doAnswer { simulateDisconnect() }
+        }
+    private val serviceComponent =
+        mock<HomeControlsRemoteServiceComponent> {
+            on { connectionManager } doReturn connectionManager
+        }
+
+    private val underTest by lazy { kosmos.remoteHomeControlsDataSourceDelegator }
+
+    @Before
+    fun setUp() {
+        kosmos.homeControlsRemoteServiceFactory =
+            mock<HomeControlsRemoteServiceComponent.Factory>().stub {
+                on { create(callbackCaptor.capture()) } doReturn serviceComponent
+            }
+    }
+
+    @Test
+    fun testQueriesComponentInfoFromBinder() =
+        testScope.runTest {
+            assertThat(fakeBinder.callbacks).isEmpty()
+
+            val componentInfo by collectLastValue(underTest.componentInfo)
+
+            assertThat(componentInfo).isNull()
+            assertThat(fakeBinder.callbacks).hasSize(1)
+
+            fakeBinder.notifyCallbacks(TEST_COMPONENT, allowTrivialControlsOnLockscreen = true)
+            assertThat(componentInfo)
+                .isEqualTo(
+                    HomeControlsComponentInfo(
+                        TEST_COMPONENT,
+                        allowTrivialControlsOnLockscreen = true,
+                    )
+                )
+        }
+
+    @Test
+    fun testOnlyConnectToServiceOnSubscription() =
+        testScope.runTest {
+            verify(connectionManager, never()).start()
+
+            val job = launch { underTest.componentInfo.collect {} }
+            runCurrent()
+            verify(connectionManager, times(1)).start()
+            verify(connectionManager, never()).stop()
+
+            job.cancel()
+            runCurrent()
+            verify(connectionManager, times(1)).start()
+            verify(connectionManager, times(1)).stop()
+        }
+
+    private fun simulateConnect() {
+        callbackCaptor.lastValue.onConnected(mock(), proxy)
+    }
+
+    private fun simulateDisconnect() {
+        callbackCaptor.lastValue.onDisconnected(mock(), 0)
+    }
+
+    private companion object {
+        val TEST_COMPONENT = ComponentName("pkg.test", "class.test")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt
new file mode 100644
index 0000000..3a2791f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.dagger
+
+import com.android.systemui.Flags.homeControlsDreamHsum
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dreams.homecontrols.service.RemoteHomeControlsDataSourceDelegator
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
+import com.android.systemui.dreams.homecontrols.system.LocalHomeControlsDataSourceDelegator
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface HomeControlsDataSourceModule {
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun providesHomeControlsDataSource(
+            localSource: Lazy<LocalHomeControlsDataSourceDelegator>,
+            remoteSource: Lazy<RemoteHomeControlsDataSourceDelegator>,
+        ): HomeControlsDataSource {
+            return if (homeControlsDreamHsum()) {
+                remoteSource.get()
+            } else {
+                localSource.get()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt
new file mode 100644
index 0000000..500d15e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.dagger
+
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dreams.homecontrols.service.HomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService
+import com.android.systemui.util.service.ObservableServiceConnection
+import com.android.systemui.util.service.Observer
+import com.android.systemui.util.service.PersistentConnectionManager
+import com.android.systemui.util.service.dagger.ObservableServiceModule
+import dagger.BindsInstance
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import javax.inject.Named
+
+/**
+ * This component is responsible for generating the connection to the home controls remote service
+ * which runs in the SYSTEM_USER context and provides the data needed to run the home controls dream
+ * in the foreground user context.
+ */
+@Subcomponent(
+    modules =
+        [
+            ObservableServiceModule::class,
+            HomeControlsRemoteServiceComponent.HomeControlsRemoteServiceModule::class,
+        ]
+)
+interface HomeControlsRemoteServiceComponent {
+    /** Creates a [HomeControlsRemoteServiceComponent]. */
+    @Subcomponent.Factory
+    interface Factory {
+        fun create(
+            @BindsInstance callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy>
+        ): HomeControlsRemoteServiceComponent
+    }
+
+    /** A [PersistentConnectionManager] pointing to the home controls remote service. */
+    val connectionManager: PersistentConnectionManager<HomeControlsRemoteProxy>
+
+    /** Scoped module providing specific components for the [ObservableServiceConnection]. */
+    @Module
+    interface HomeControlsRemoteServiceModule {
+        companion object {
+            @Provides
+            @Named(ObservableServiceModule.SERVICE_CONNECTION)
+            fun providesConnection(
+                connection: ObservableServiceConnection<HomeControlsRemoteProxy>,
+                callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy>,
+            ): ObservableServiceConnection<HomeControlsRemoteProxy> {
+                connection.addCallback(callback)
+                return connection
+            }
+
+            /** Provides the wrapper around the home controls remote binder */
+            @Provides
+            fun providesTransformer(
+                factory: HomeControlsRemoteProxy.Factory
+            ): ObservableServiceConnection.ServiceTransformer<HomeControlsRemoteProxy> {
+                return ObservableServiceConnection.ServiceTransformer { service: IBinder ->
+                    factory.create(IHomeControlsRemoteProxy.Stub.asInterface(service))
+                }
+            }
+
+            /** Provides the intent to connect to [HomeControlsRemoteService] */
+            @Provides
+            fun providesIntent(@Application context: Context): Intent {
+                return Intent(context, HomeControlsRemoteService::class.java)
+            }
+
+            /** Provides no-op [Observer] since the remote service is in the same package */
+            @Provides
+            @Named(ObservableServiceModule.OBSERVER)
+            fun providesObserver(): Observer {
+                return object : Observer {
+                    override fun addCallback(callback: Observer.Callback?) {
+                        // no-op, do nothing
+                    }
+
+                    override fun removeCallback(callback: Observer.Callback?) {
+                        // no-op, do nothing
+                    }
+                }
+            }
+
+            /**
+             * Provides a name that will be used by [PersistentConnectionManager] when logging
+             * state.
+             */
+            @Provides
+            @Named(ObservableServiceModule.DUMPSYS_NAME)
+            fun providesDumpsysName(): String {
+                return "HomeControlsRemoteService"
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt
new file mode 100644
index 0000000..2bcfea8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.service
+
+import android.content.ComponentName
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy
+import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.stateIn
+
+/** Class to wrap [IHomeControlsRemoteProxy], which exposes the current user's home controls info */
+class HomeControlsRemoteProxy
+@AssistedInject
+constructor(
+    @Background bgScope: CoroutineScope,
+    dumpManager: DumpManager,
+    @Assisted private val proxy: IHomeControlsRemoteProxy,
+) : FlowDumperImpl(dumpManager) {
+
+    private companion object {
+        const val TAG = "HomeControlsRemoteProxy"
+    }
+
+    val componentInfo: Flow<HomeControlsComponentInfo> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : IOnControlsSettingsChangeListener.Stub() {
+                        override fun onControlsSettingsChanged(
+                            panelComponent: ComponentName?,
+                            allowTrivialControlsOnLockscreen: Boolean,
+                        ) {
+                            trySendWithFailureLogging(
+                                HomeControlsComponentInfo(
+                                    panelComponent,
+                                    allowTrivialControlsOnLockscreen,
+                                ),
+                                TAG,
+                            )
+                        }
+                    }
+                proxy.registerListenerForCurrentUser(listener)
+                awaitClose { proxy.unregisterListenerForCurrentUser(listener) }
+            }
+            .distinctUntilChanged()
+            .stateIn(bgScope, SharingStarted.WhileSubscribed(), null)
+            .dumpValue("componentInfo")
+            .filterNotNull()
+
+    @AssistedFactory
+    interface Factory {
+        fun create(proxy: IHomeControlsRemoteProxy): HomeControlsRemoteProxy
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt
new file mode 100644
index 0000000..b14903d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.service
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dreams.DreamLogger
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.DreamLog
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.service.ObservableServiceConnection
+import com.android.systemui.util.service.PersistentConnectionManager
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/**
+ * Queries a remote service for [HomeControlsComponentInfo] necessary to show the home controls
+ * dream.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class RemoteHomeControlsDataSourceDelegator
+@Inject
+constructor(
+    @Background bgScope: CoroutineScope,
+    serviceFactory: HomeControlsRemoteServiceComponent.Factory,
+    @DreamLog logBuffer: LogBuffer,
+    dumpManager: DumpManager,
+) : HomeControlsDataSource, FlowDumperImpl(dumpManager) {
+    private val logger = DreamLogger(logBuffer, TAG)
+
+    private val connectionManager: PersistentConnectionManager<HomeControlsRemoteProxy> by lazy {
+        serviceFactory.create(callback).connectionManager
+    }
+
+    private val proxyState =
+        MutableStateFlow<HomeControlsRemoteProxy?>(null)
+            .apply {
+                subscriptionCount
+                    .map { it > 0 }
+                    .dropWhile { !it }
+                    .distinctUntilChanged()
+                    .onEach { active ->
+                        logger.d({ "Remote service connection active: $bool1" }) { bool1 = active }
+                        if (active) {
+                            connectionManager.start()
+                        } else {
+                            connectionManager.stop()
+                        }
+                    }
+                    .launchIn(bgScope)
+            }
+            .dumpValue("proxyState")
+
+    private val callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy> =
+        object : ObservableServiceConnection.Callback<HomeControlsRemoteProxy> {
+            override fun onConnected(
+                connection: ObservableServiceConnection<HomeControlsRemoteProxy>?,
+                proxy: HomeControlsRemoteProxy,
+            ) {
+                logger.d("Service connected")
+                proxyState.value = proxy
+            }
+
+            override fun onDisconnected(
+                connection: ObservableServiceConnection<HomeControlsRemoteProxy>?,
+                reason: Int,
+            ) {
+                logger.d({ "Service disconnected with reason $int1" }) { int1 = reason }
+                proxyState.value = null
+            }
+        }
+
+    override val componentInfo: Flow<HomeControlsComponentInfo> =
+        proxyState
+            .filterNotNull()
+            .flatMapLatest { proxy: HomeControlsRemoteProxy -> proxy.componentInfo }
+            .dumpWhileCollecting("componentInfo")
+
+    private companion object {
+        const val TAG = "HomeControlsRemoteDataSourceDelegator"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt
new file mode 100644
index 0000000..b9e5080
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols.shared.model
+
+import android.content.ComponentName
+
+data class HomeControlsComponentInfo(
+    val componentName: ComponentName?,
+    val allowTrivialControlsOnLockscreen: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt
new file mode 100644
index 0000000..8187c54
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.homecontrols.shared.model
+
+import kotlinx.coroutines.flow.Flow
+
+/** Source of data for home controls dream to get the necessary information it needs. */
+interface HomeControlsDataSource {
+    val componentInfo: Flow<HomeControlsComponentInfo>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt
new file mode 100644
index 0000000..ca255fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.system
+
+import com.android.systemui.controls.settings.ControlsSettingsRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo
+import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource
+import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Queries local data sources for the [HomeControlsComponentInfo] necessary to show the home
+ * controls dream.
+ */
+@SysUISingleton
+class LocalHomeControlsDataSourceDelegator
+@Inject
+constructor(
+    homeControlsComponentInteractor: HomeControlsComponentInteractor,
+    controlsSettingsRepository: ControlsSettingsRepository,
+) : HomeControlsDataSource {
+    override val componentInfo: Flow<HomeControlsComponentInfo> =
+        combine(
+            homeControlsComponentInteractor.panelComponent,
+            controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen,
+        ) { panelComponent, allowActionOnTrivialControlsInLockscreen ->
+            HomeControlsComponentInfo(panelComponent, allowActionOnTrivialControlsInLockscreen)
+        }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt
new file mode 100644
index 0000000..58ebbf4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.service
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.homeControlsRemoteProxy by
+    Kosmos.Fixture {
+        HomeControlsRemoteProxy(
+            bgScope = applicationCoroutineScope,
+            proxy = fakeHomeControlsRemoteBinder,
+            dumpManager = dumpManager,
+        )
+    }
+
+val Kosmos.fakeHomeControlsRemoteBinder by
+    Kosmos.Fixture<FakeIHomeControlsRemoteProxyBinder> { FakeIHomeControlsRemoteProxyBinder() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt
new file mode 100644
index 0000000..c85c888
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.service
+
+import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.log.logcatLogBuffer
+import org.mockito.kotlin.mock
+
+val Kosmos.remoteHomeControlsDataSourceDelegator by
+    Kosmos.Fixture {
+        RemoteHomeControlsDataSourceDelegator(
+            bgScope = applicationCoroutineScope,
+            serviceFactory = homeControlsRemoteServiceFactory,
+            logBuffer = logcatLogBuffer("HomeControlsDreamInteractor"),
+            dumpManager = dumpManager,
+        )
+    }
+
+var Kosmos.homeControlsRemoteServiceFactory by
+    Kosmos.Fixture<HomeControlsRemoteServiceComponent.Factory> { mock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt
new file mode 100644
index 0000000..f8ea022
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.shared.model
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+
+class FakeHomeControlsDataSource : HomeControlsDataSource {
+
+    private val _componentInfo = MutableStateFlow<HomeControlsComponentInfo?>(null)
+
+    override val componentInfo: Flow<HomeControlsComponentInfo>
+        get() = _componentInfo.filterNotNull()
+
+    fun setComponentInfo(info: HomeControlsComponentInfo) {
+        _componentInfo.value = info
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt
new file mode 100644
index 0000000..942216b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.shared.model
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.homeControlsDataSource by
+    Kosmos.Fixture<HomeControlsDataSource> { fakeHomeControlsDataSource }
+
+val Kosmos.fakeHomeControlsDataSource by
+    Kosmos.Fixture<FakeHomeControlsDataSource> { FakeHomeControlsDataSource() }