Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0b440fd9 authored by Lucas Silva's avatar Lucas Silva
Browse files

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
parent e3ed9b0a
Loading
Loading
Loading
Loading
+95 −0
Original line number Diff line number Diff line
/*
 * 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")
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * 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")
    }
}
+44 −0
Original line number Diff line number Diff line
/*
 * 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()
            }
        }
    }
}
+116 −0
Original line number Diff line number Diff line
/*
 * 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"
            }
        }
    }
}
+81 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
Loading