diff options
31 files changed, 1500 insertions, 435 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 4531b7932eaf..7317a756a615 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -1137,5 +1137,11 @@ android:name="android.service.dream" android:resource="@xml/home_controls_dream_metadata" /> </service> + + <service android:name="com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService" + android:singleUser="true" + android:exported="false" + /> + </application> </manifest> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7921ce0e1291..261929c0bd2c 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1615,6 +1615,16 @@ flag { } flag { + name: "home_controls_dream_hsum" + namespace: "systemui" + description: "Enables the home controls dream in HSUM" + bug: "370691405" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "only_show_media_stream_slider_in_single_volume_mode" namespace: "systemui" description: "When the device is in single volume mode, only show media stream slider and hide all other stream (e.g. call, notification, alarm, etc) sliders in volume panel" 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 index 9300db9a24c8..4317b9f27da6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt @@ -16,25 +16,37 @@ package com.android.systemui.dreams.homecontrols import android.app.Activity +import android.content.ComponentName import android.content.Intent +import android.os.powerManager import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_DREAM import android.service.controls.ControlsProviderService.EXTRA_CONTROLS_SURFACE +import android.service.dreams.DreamService import android.window.TaskFragmentInfo +import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.dreams.homecontrols.shared.model.fakeHomeControlsDataSource +import com.android.systemui.dreams.homecontrols.shared.model.homeControlsDataSource import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.android.systemui.util.wakelock.WakeLockFake import com.google.common.truth.Truth.assertThat -import java.util.Optional +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +59,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -62,13 +73,18 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { WakeLockFake.Builder(context).apply { setWakeLock(fakeWakeLock) } } + private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher) + private val taskFragmentComponent = mock<TaskFragmentComponent>() private val activity = mock<Activity>() private val onCreateCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>() private val onInfoChangedCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>() private val hideCallback = argumentCaptor<() -> Unit>() - private val dreamServiceDelegate = - mock<DreamServiceDelegate> { on { getActivity(any()) } doReturn activity } + private var dreamService = + mock<DreamService> { + on { activity } doReturn activity + on { redirectWake } doReturn false + } private val taskFragmentComponentFactory = mock<TaskFragmentComponent.Factory> { @@ -82,12 +98,32 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { } doReturn taskFragmentComponent } - private val underTest: HomeControlsDreamService by lazy { buildService() } + private val underTest: HomeControlsDreamServiceImpl by lazy { + with(kosmos) { + HomeControlsDreamServiceImpl( + taskFragmentFactory = taskFragmentComponentFactory, + wakeLockBuilder = fakeWakeLockBuilder, + powerManager = powerManager, + systemClock = fakeSystemClock, + dataSource = homeControlsDataSource, + logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest"), + service = dreamService, + lifecycleOwner = lifecycleOwner, + ) + } + } @Before fun setup() { - whenever(kosmos.controlsComponent.getControlsListingController()) - .thenReturn(Optional.of(kosmos.controlsListingController)) + Dispatchers.setMain(kosmos.testDispatcher) + kosmos.fakeHomeControlsDataSource.setComponentInfo( + HomeControlsComponentInfo(PANEL_COMPONENT, true) + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() } @Test @@ -108,13 +144,10 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { @Test fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() = testScope.runTest { - val serviceWithNullActivity = - buildService( - mock<DreamServiceDelegate> { on { getActivity(underTest) } doReturn null } - ) - - serviceWithNullActivity.onAttachedToWindow() + dreamService = mock<DreamService> { on { activity } doReturn null } + underTest.onAttachedToWindow() verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any()) + verify(dreamService).finish() } @Test @@ -137,9 +170,9 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { @Test fun testFinishesDreamWithoutRestartingActivityWhenNotRedirectingWakes() = testScope.runTest { - whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(false) underTest.onAttachedToWindow() onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>()) + runCurrent() verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) // Task fragment becomes empty @@ -149,16 +182,21 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { advanceUntilIdle() // Dream is finished and activity is not restarted verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) - verify(dreamServiceDelegate, never()).wakeUp(any()) - verify(dreamServiceDelegate).finish(any()) + verify(dreamService, never()).wakeUp() + verify(dreamService).finish() } @Test fun testRestartsActivityWhenRedirectingWakes() = testScope.runTest { - whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(true) + dreamService = + mock<DreamService> { + on { activity } doReturn activity + on { redirectWake } doReturn true + } underTest.onAttachedToWindow() onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>()) + runCurrent() verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) // Task fragment becomes empty @@ -166,30 +204,20 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { mock<TaskFragmentInfo> { on { isEmpty } doReturn true } ) advanceUntilIdle() + // Activity is restarted instead of finishing the dream. verify(taskFragmentComponent, times(2)).startActivityInTaskFragment(intentMatcher()) - verify(dreamServiceDelegate).wakeUp(any()) - verify(dreamServiceDelegate, never()).finish(any()) + verify(dreamService).wakeUp() + verify(dreamService, never()).finish() } private fun intentMatcher() = argThat<Intent> { getIntExtra(EXTRA_CONTROLS_SURFACE, CONTROLS_SURFACE_ACTIVITY_PANEL) == - CONTROLS_SURFACE_DREAM + CONTROLS_SURFACE_DREAM && component == PANEL_COMPONENT } - private fun buildService( - activityProvider: DreamServiceDelegate = dreamServiceDelegate - ): HomeControlsDreamService = - with(kosmos) { - return HomeControlsDreamService( - controlsSettingsRepository = FakeControlsSettingsRepository(), - taskFragmentFactory = taskFragmentComponentFactory, - homeControlsComponentInteractor = homeControlsComponentInteractor, - wakeLockBuilder = fakeWakeLockBuilder, - dreamServiceDelegate = activityProvider, - bgDispatcher = testDispatcher, - logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest") - ) - } + private companion object { + val PANEL_COMPONENT = ComponentName("test.pkg", "test.panel") + } } 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 index 1adf414c9ef0..ed45e8cff1f4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt @@ -34,9 +34,14 @@ import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.SelectedComponentRepository 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.dreams.homecontrols.system.HomeControlsDreamStartable +import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController +import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository @@ -96,8 +101,9 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { HomeControlsDreamStartable( mContext, packageManager, + kosmos.userTracker, homeControlsComponentInteractor, - kosmos.applicationCoroutineScope + kosmos.applicationCoroutineScope, ) } @@ -113,7 +119,7 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { .setComponentEnabledSetting( eq(componentName), eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), - eq(PackageManager.DONT_KILL_APP) + eq(PackageManager.DONT_KILL_APP), ) } @@ -128,7 +134,7 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { .setComponentEnabledSetting( eq(componentName), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - eq(PackageManager.DONT_KILL_APP) + eq(PackageManager.DONT_KILL_APP), ) } @@ -143,14 +149,14 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { .setComponentEnabledSetting( eq(componentName), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - eq(PackageManager.DONT_KILL_APP) + eq(PackageManager.DONT_KILL_APP), ) } private fun ControlsServiceInfo( componentName: ComponentName, label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ): ControlsServiceInfo { val serviceInfo = ServiceInfo().apply { @@ -165,7 +171,7 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { context: Context, serviceInfo: ServiceInfo, private val label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ) : ControlsServiceInfo(context, serviceInfo) { init { @@ -185,7 +191,7 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { UserInfo( /* id= */ PRIMARY_USER_ID, /* name= */ "primary user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val TEST_PACKAGE_PANEL = "pkg.panel" private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service") @@ -193,13 +199,13 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { SelectedComponentRepository.SelectedComponent( TEST_PACKAGE_PANEL, TEST_COMPONENT_PANEL, - true + true, ) private val TEST_SELECTED_COMPONENT_NON_PANEL = SelectedComponentRepository.SelectedComponent( TEST_PACKAGE_PANEL, TEST_COMPONENT_PANEL, - false + false, ) } } 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 000000000000..e57776f4db1b --- /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 000000000000..400217503299 --- /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/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt new file mode 100644 index 000000000000..f8a45e82c2ab --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt @@ -0,0 +1,236 @@ +/* + * 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 android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.content.pm.UserInfo +import androidx.lifecycle.testing.TestLifecycleOwner +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.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.authorizedPanelsRepository +import com.android.systemui.controls.panels.selectedComponentRepository +import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController +import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.test.TestScope +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.Mockito + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class HomeControlsRemoteServiceBinderTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher) + private val fakeControlsSettingsRepository = FakeControlsSettingsRepository() + + private val underTest by lazy { + HomeControlsRemoteServiceBinder( + kosmos.homeControlsComponentInteractor, + fakeControlsSettingsRepository, + kosmos.backgroundCoroutineContext, + logcatLogBuffer(), + lifecycleOwner, + ) + } + + @Before + fun setUp() { + with(kosmos) { + fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + } + } + + @Test + fun testRegisterSingleListener() = + testScope.runTest { + setup() + val controlsSettings by collectLastValue(addCallback()) + runServicesUpdate() + + assertThat(controlsSettings) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + } + + @Test + fun testRegisterMultipleListeners() = + testScope.runTest { + setup() + val controlsSettings1 by collectLastValue(addCallback()) + val controlsSettings2 by collectLastValue(addCallback()) + runServicesUpdate() + + assertThat(controlsSettings1) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + assertThat(controlsSettings2) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + } + + @Test + fun testListenerCalledWhenStateChanges() = + testScope.runTest { + setup() + val controlsSettings by collectLastValue(addCallback()) + runServicesUpdate() + + assertThat(controlsSettings) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + + kosmos.authorizedPanelsRepository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) + + // Updated with null component now that we are no longer authorized. + assertThat(controlsSettings) + .isEqualTo( + CallbackArgs(panelComponent = null, allowTrivialControlsOnLockscreen = false) + ) + } + + private fun TestScope.runServicesUpdate() { + runCurrent() + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) + val callback = withArgCaptor { + Mockito.verify(kosmos.controlsListingController).addCallback(capture()) + } + callback.onServicesUpdated(listings) + runCurrent() + } + + private fun addCallback() = conflatedCallbackFlow { + val callback = + object : IOnControlsSettingsChangeListener.Stub() { + override fun onControlsSettingsChanged( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + trySend(CallbackArgs(panelComponent, allowTrivialControlsOnLockscreen)) + } + } + underTest.registerListenerForCurrentUser(callback) + awaitClose { underTest.unregisterListenerForCurrentUser(callback) } + } + + private suspend fun TestScope.setup() { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserTracker.set(listOf(PRIMARY_USER), 0) + kosmos.authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + kosmos.selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + runCurrent() + } + + private data class CallbackArgs( + val panelComponent: ComponentName?, + val allowTrivialControlsOnLockscreen: Boolean, + ) + + 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 + } + } + + private companion object { + const val PRIMARY_USER_ID = 0 + val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY, + ) + + private const val TEST_PACKAGE = "pkg" + private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service") + private val TEST_SELECTED_COMPONENT_PANEL = + SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, true) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt index 7292985b2dba..c950523f7854 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt @@ -20,40 +20,32 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.ServiceInfo import android.content.pm.UserInfo -import android.os.PowerManager -import android.os.UserHandle -import android.os.powerManager -import android.service.dream.dreamManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.data.repository.fakePackageChangeRepository import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController +import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor -import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope 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.ArgumentMatchers.anyInt -import org.mockito.Mockito.anyLong -import org.mockito.Mockito.never import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @@ -68,7 +60,6 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { @Before fun setUp() = with(kosmos) { - fakeSystemClock.setCurrentTimeMillis(0) fakeUserRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) whenever(controlsComponent.getControlsListingController()) .thenReturn(Optional.of(controlsListingController)) @@ -172,113 +163,6 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { } } - @Test - fun testMonitoringUpdatesAndRestart() = - with(kosmos) { - testScope.runTest { - setActiveUser(PRIMARY_USER) - authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) - selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) - whenever(controlsListingController.getCurrentServices()) - .thenReturn( - listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) - ) - - val job = launch { underTest.monitorUpdatesAndRestart() } - val panelComponent by collectLastValue(underTest.panelComponent) - - assertThat(panelComponent).isEqualTo(TEST_COMPONENT) - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(100) - // The package update is started. - fakePackageChangeRepository.notifyUpdateStarted( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds) - // Task fragment becomes empty as a result of the update. - underTest.onDreamEndUnexpectedly() - - runCurrent() - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(500) - // The package update is finished. - fakePackageChangeRepository.notifyUpdateFinished( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - - runCurrent() - verify(dreamManager).startDream() - job.cancel() - } - } - - @Test - fun testMonitoringUpdatesAndRestart_dreamEndsAfterDelay() = - with(kosmos) { - testScope.runTest { - setActiveUser(PRIMARY_USER) - authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) - selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) - whenever(controlsListingController.getCurrentServices()) - .thenReturn( - listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) - ) - - val job = launch { underTest.monitorUpdatesAndRestart() } - val panelComponent by collectLastValue(underTest.panelComponent) - - assertThat(panelComponent).isEqualTo(TEST_COMPONENT) - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(100) - // The package update is started. - fakePackageChangeRepository.notifyUpdateStarted( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds + 100) - // Task fragment becomes empty as a result of the update. - underTest.onDreamEndUnexpectedly() - - runCurrent() - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(500) - // The package update is finished. - fakePackageChangeRepository.notifyUpdateFinished( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - - runCurrent() - verify(dreamManager, never()).startDream() - job.cancel() - } - } - - @Test - fun testDreamUnexpectedlyEnds_triggersUserActivity() = - with(kosmos) { - testScope.runTest { - fakeSystemClock.setUptimeMillis(100000L) - verify(powerManager, never()).userActivity(anyLong(), anyInt(), anyInt()) - - // Dream ends unexpectedly - underTest.onDreamEndUnexpectedly() - - verify(powerManager) - .userActivity( - 100000L, - PowerManager.USER_ACTIVITY_EVENT_OTHER, - PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS - ) - } - } - private fun runServicesUpdate(hasPanelBoolean: Boolean = true) { val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = hasPanelBoolean)) @@ -297,7 +181,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { private fun ControlsServiceInfo( componentName: ComponentName, label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ): ControlsServiceInfo { val serviceInfo = ServiceInfo().apply { @@ -312,7 +196,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { context: Context, serviceInfo: ServiceInfo, private val label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ) : ControlsServiceInfo(context, serviceInfo) { init { @@ -332,7 +216,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { UserInfo( /* id= */ PRIMARY_USER_ID, /* name= */ "primary user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val ANOTHER_USER_ID = 1 @@ -340,7 +224,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { UserInfo( /* id= */ ANOTHER_USER_ID, /* name= */ "another user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val TEST_PACKAGE = "pkg" private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service") diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 6fb6236a4ed3..4cdf28670eab 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -29,7 +29,7 @@ import com.android.systemui.controls.dagger.StartControlsStartableModule 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.dreams.homecontrols.system.HomeControlsDreamStartable import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.haptics.msdl.MSDLCoreStartable import com.android.systemui.keyboard.KeyboardUI 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 a45ad157837b..3171bbc6d6b0 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -33,9 +33,10 @@ import com.android.systemui.dreams.DreamOverlayNotificationCountProvider; import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dreams.SystemDialogsCloser; import com.android.systemui.dreams.complication.dagger.ComplicationComponent; -import com.android.systemui.dreams.homecontrols.DreamServiceDelegate; -import com.android.systemui.dreams.homecontrols.DreamServiceDelegateImpl; import com.android.systemui.dreams.homecontrols.HomeControlsDreamService; +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsDataSourceModule; +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent; +import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.pipeline.shared.TileSpec; import com.android.systemui.qs.shared.model.TileCategory; @@ -61,13 +62,15 @@ import javax.inject.Named; * Dagger Module providing Dream-related functionality. */ @Module(includes = { - RegisteredComplicationsModule.class, - LowLightDreamModule.class, - ScrimModule.class - }, + RegisteredComplicationsModule.class, + LowLightDreamModule.class, + ScrimModule.class, + HomeControlsDataSourceModule.class, +}, subcomponents = { - ComplicationComponent.class, - DreamOverlayComponent.class, + ComplicationComponent.class, + DreamOverlayComponent.class, + HomeControlsRemoteServiceComponent.class, }) public interface DreamModule { String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user"; @@ -113,6 +116,15 @@ public interface DreamModule { HomeControlsDreamService service); /** + * Provides Home Controls Remote Service + */ + @Binds + @IntoMap + @ClassKey(HomeControlsRemoteService.class) + Service bindHomeControlsRemoteService( + HomeControlsRemoteService service); + + /** * Provides a touch inset manager for dreams. */ @Provides @@ -202,10 +214,4 @@ public interface DreamModule { QSTilePolicy.NoRestrictions.INSTANCE ); } - - - /** Provides delegate to allow for testing of dream service */ - @Binds - DreamServiceDelegate bindDreamDelegate(DreamServiceDelegateImpl impl); - } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt deleted file mode 100644 index 2cfb02eadd19..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 - -/** Provides abstraction for [DreamService] methods, so they can be mocked in tests. */ -interface DreamServiceDelegate { - /** Wrapper for [DreamService.getActivity] which can be mocked in tests. */ - fun getActivity(dreamService: DreamService): Activity? - - /** Wrapper for [DreamService.wakeUp] which can be mocked in tests. */ - fun wakeUp(dreamService: DreamService) - - /** Wrapper for [DreamService.finish] which can be mocked in tests. */ - fun finish(dreamService: DreamService) - - /** Wrapper for [DreamService.getRedirectWake] which can be mocked in tests. */ - fun redirectWake(dreamService: DreamService): Boolean -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt index 6b14ebf31d84..9eec1b617792 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt @@ -16,75 +16,109 @@ package com.android.systemui.dreams.homecontrols +import android.annotation.SuppressLint import android.content.Intent import android.os.PowerManager 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.coroutines.newTracingContext -import com.android.systemui.dagger.qualifiers.Background +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ServiceLifecycleDispatcher +import androidx.lifecycle.lifecycleScope import com.android.systemui.dreams.DreamLogger -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY +import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.DreamLog +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.wakelock.WakeLock -import com.android.systemui.util.wakelock.WakeLock.Builder.NO_TIMEOUT +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +/** + * [DreamService] which embeds the user's chosen home controls app to allow it to display as a + * screensaver. This service will run in the foreground user context. + */ class HomeControlsDreamService @Inject +constructor(private val factory: HomeControlsDreamServiceImpl.Factory) : + DreamService(), LifecycleOwner { + + private val dispatcher = ServiceLifecycleDispatcher(this) + override val lifecycle: Lifecycle + get() = dispatcher.lifecycle + + private val impl: HomeControlsDreamServiceImpl by lazy { factory.create(this, this) } + + override fun onCreate() { + dispatcher.onServicePreSuperOnCreate() + super.onCreate() + } + + override fun onDreamingStarted() { + dispatcher.onServicePreSuperOnStart() + super.onDreamingStarted() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + impl.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + dispatcher.onServicePreSuperOnDestroy() + super.onDetachedFromWindow() + impl.onDetachedFromWindow() + } +} + +/** + * Implementation of the home controls dream service, which allows for injecting a [DreamService] + * and [LifecycleOwner] for testing. + */ +class HomeControlsDreamServiceImpl +@AssistedInject constructor( - private val controlsSettingsRepository: ControlsSettingsRepository, private val taskFragmentFactory: TaskFragmentComponent.Factory, - private val homeControlsComponentInteractor: HomeControlsComponentInteractor, private val wakeLockBuilder: WakeLock.Builder, - private val dreamServiceDelegate: DreamServiceDelegate, - @Background private val bgDispatcher: CoroutineDispatcher, - @DreamLog logBuffer: LogBuffer -) : DreamService() { - - private val serviceJob = SupervisorJob() - private val serviceScope = - CoroutineScope(bgDispatcher + serviceJob + newTracingContext("HomeControlsDreamService")) + private val powerManager: PowerManager, + private val systemClock: SystemClock, + private val dataSource: HomeControlsDataSource, + @DreamLog logBuffer: LogBuffer, + @Assisted private val service: DreamService, + @Assisted lifecycleOwner: LifecycleOwner, +) : LifecycleOwner by lifecycleOwner { + private val logger = DreamLogger(logBuffer, TAG) private lateinit var taskFragmentComponent: TaskFragmentComponent private val wakeLock: WakeLock by lazy { wakeLockBuilder - .setMaxTimeout(NO_TIMEOUT) + .setMaxTimeout(WakeLock.Builder.NO_TIMEOUT) .setTag(TAG) .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) .build() } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - val activity = dreamServiceDelegate.getActivity(this) + fun onAttachedToWindow() { + val activity = service.activity if (activity == null) { - finish() + service.finish() return } - - // Start monitoring package updates to possibly restart the dream if the home controls - // package is updated while we are dreaming. - serviceScope.launch { homeControlsComponentInteractor.monitorUpdatesAndRestart() } - taskFragmentComponent = taskFragmentFactory .create( activity = activity, onCreateCallback = { launchActivity() }, onInfoChangedCallback = this::onTaskFragmentInfoChanged, - hide = { endDream(false) } + hide = { endDream(false) }, ) .apply { createTaskFragment() } @@ -99,53 +133,61 @@ constructor( } private fun endDream(handleRedirect: Boolean) { - homeControlsComponentInteractor.onDreamEndUnexpectedly() - if (handleRedirect && dreamServiceDelegate.redirectWake(this)) { - dreamServiceDelegate.wakeUp(this) - serviceScope.launch { + pokeUserActivity() + if (handleRedirect && service.redirectWake) { + service.wakeUp() + lifecycleScope.launch { delay(ACTIVITY_RESTART_DELAY) launchActivity() } } else { - dreamServiceDelegate.finish(this) + service.finish() } } private fun launchActivity() { - 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) + lifecycleScope.launch { + val (componentName, setting) = dataSource.componentInfo.first() + 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() + fun onDetachedFromWindow() { wakeLock.release(TAG) taskFragmentComponent.destroy() - serviceScope.launch { - delay(CANCELLATION_DELAY_AFTER_DETACHED) - serviceJob.cancel("Dream detached from window") - } } - private companion object { - /** - * Defines how long after the dream ends that we should keep monitoring for package updates - * to attempt a restart of the dream. This should be larger than - * [MAX_UPDATE_CORRELATION_DELAY] as it also includes the time the package update takes to - * complete. - */ - val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds + @SuppressLint("MissingPermission") + private fun pokeUserActivity() { + powerManager.userActivity( + systemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_OTHER, + PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS, + ) + } + + @AssistedFactory + interface Factory { + fun create( + service: DreamService, + lifecycleOwner: LifecycleOwner, + ): HomeControlsDreamServiceImpl + } + companion object { /** * Defines the delay after wakeup where we should attempt to restart the embedded activity. * When a wakeup is redirected, the dream service may keep running. In this case, we should @@ -153,6 +195,6 @@ constructor( * after the wakeup transition has played. */ val ACTIVITY_RESTART_DELAY = 334.milliseconds - const val TAG = "HomeControlsDreamService" + private const val TAG = "HomeControlsDreamServiceImpl" } } 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 000000000000..3a2791fc503f --- /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 000000000000..500d15e56498 --- /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 000000000000..2bcfea8c1179 --- /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 000000000000..b14903d7885f --- /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/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt index d547de24beb5..67de30c8fa5c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt @@ -14,29 +14,18 @@ * limitations under the License. */ -package com.android.systemui.dreams.homecontrols +package com.android.systemui.dreams.homecontrols.service import android.app.Activity -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration 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 @@ -65,7 +54,7 @@ constructor( activity: Activity, @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback, @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback, - hide: () -> Unit + hide: () -> Unit, ): TaskFragmentComponent } @@ -90,26 +79,28 @@ constructor( change.taskFragmentInfo?.let { taskFragmentInfo -> if (taskFragmentInfo.fragmentToken == fragmentToken) { when (change.type) { - TYPE_TASK_FRAGMENT_APPEARED -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED -> { resultT.addTaskFragmentOperation( fragmentToken, - TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK) - .build() + TaskFragmentOperation.Builder( + TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK + ) + .build(), ) onCreateCallback(taskFragmentInfo) } - TYPE_TASK_FRAGMENT_INFO_CHANGED -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED -> { onInfoChangedCallback(taskFragmentInfo) } - TYPE_TASK_FRAGMENT_VANISHED -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED -> { hide() } - TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {} - TYPE_TASK_FRAGMENT_ERROR -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {} + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR -> { hide() } - TYPE_ACTIVITY_REPARENTED_TO_TASK -> {} + TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK -> {} else -> throw IllegalArgumentException( "Unknown TaskFragmentEvent=" + change.type @@ -121,8 +112,8 @@ constructor( organizer.onTransactionHandled( transaction.transactionToken, resultT, - TASK_FRAGMENT_TRANSIT_CHANGE, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false, ) } @@ -132,15 +123,15 @@ constructor( TaskFragmentCreationParams.Builder( organizer.organizerToken, fragmentToken, - activity.activityToken!! + activity.activityToken!!, ) .setInitialRelativeBounds(Rect()) - .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN) .build() organizer.applyTransaction( WindowContainerTransaction().createTaskFragment(fragmentOptions), - TASK_FRAGMENT_TRANSIT_CHANGE, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false, ) } @@ -151,8 +142,8 @@ constructor( fun startActivityInTaskFragment(intent: Intent) { organizer.applyTransaction( WindowContainerTransaction().startActivity(intent), - TASK_FRAGMENT_TRANSIT_OPEN, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN, + false, ) } @@ -162,10 +153,13 @@ constructor( WindowContainerTransaction() .addTaskFragmentOperation( fragmentToken, - TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build() + TaskFragmentOperation.Builder( + TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT + ) + .build(), ), - TASK_FRAGMENT_TRANSIT_CLOSE, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE, + false, ) organizer.unregisterOrganizer() } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl new file mode 100644 index 000000000000..115b62ca87a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl @@ -0,0 +1,9 @@ +package com.android.systemui.dreams.homecontrols.shared; + +import android.os.IRemoteCallback; +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener; + +oneway interface IHomeControlsRemoteProxy { + void registerListenerForCurrentUser(in IOnControlsSettingsChangeListener callback); + void unregisterListenerForCurrentUser(in IOnControlsSettingsChangeListener callback); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl new file mode 100644 index 000000000000..99e5fae97e45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl @@ -0,0 +1,7 @@ +package com.android.systemui.dreams.homecontrols.shared; + +import android.content.ComponentName; + +oneway interface IOnControlsSettingsChangeListener { + void onControlsSettingsChanged(in ComponentName panelComponent, boolean allowTrivialControlsOnLockscreen); +} 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 000000000000..b9e5080e92a9 --- /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 000000000000..8187c5412fa4 --- /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/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt index 03f58aca9fc6..644d5fd781c5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt @@ -14,24 +14,28 @@ * limitations under the License. */ -package com.android.systemui.dreams.homecontrols +package com.android.systemui.dreams.homecontrols.system import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.service.controls.flags.Flags.homePanelDream +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable +import com.android.systemui.Flags.homeControlsDreamHsum import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.dreams.homecontrols.HomeControlsDreamService +import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.settings.UserContextProvider import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch class HomeControlsDreamStartable @Inject constructor( context: Context, - private val packageManager: PackageManager, + private val systemPackageManager: PackageManager, + private val userContextProvider: UserContextProvider, private val homeControlsComponentInteractor: HomeControlsComponentInteractor, @Background private val bgScope: CoroutineScope, ) : CoreStartable { @@ -57,10 +61,16 @@ constructor( } else { PackageManager.COMPONENT_ENABLED_STATE_DISABLED } + val packageManager = + if (homeControlsDreamHsum()) { + userContextProvider.userContext.packageManager + } else { + systemPackageManager + } packageManager.setComponentEnabledSetting( componentName, packageState, - PackageManager.DONT_KILL_APP + PackageManager.DONT_KILL_APP, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt new file mode 100644 index 000000000000..a65d216aa5a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt @@ -0,0 +1,155 @@ +/* + * 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 android.content.ComponentName +import android.content.Intent +import android.os.IBinder +import android.os.RemoteCallbackList +import android.os.RemoteException +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.systemui.controls.settings.ControlsSettingsRepository +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dreams.DreamLogger +import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener +import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.dagger.DreamLog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch + +/** + * Service which exports the current home controls component name, for use in SystemUI processes + * running in other users. This service should only run in the system user. + */ +class HomeControlsRemoteService +@Inject +constructor(binderFactory: HomeControlsRemoteServiceBinder.Factory) : LifecycleService() { + val binder by lazy { binderFactory.create(this) } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return binder + } +} + +class HomeControlsRemoteServiceBinder +@AssistedInject +constructor( + private val homeControlsComponentInteractor: HomeControlsComponentInteractor, + private val controlsSettingsRepository: ControlsSettingsRepository, + @Background private val bgContext: CoroutineContext, + @DreamLog logBuffer: LogBuffer, + @Assisted lifecycleOwner: LifecycleOwner, +) : IHomeControlsRemoteProxy.Stub(), LifecycleOwner by lifecycleOwner { + private val logger = DreamLogger(logBuffer, TAG) + private val callbacks = + object : RemoteCallbackList<IOnControlsSettingsChangeListener>() { + override fun onCallbackDied(listener: IOnControlsSettingsChangeListener?) { + if (callbackCount.decrementAndGet() == 0) { + logger.d("Cancelling collection due to callback death") + collectionJob?.cancel() + collectionJob = null + } + } + } + private val callbackCount = AtomicInteger(0) + private var collectionJob: Job? = null + + override fun registerListenerForCurrentUser(listener: IOnControlsSettingsChangeListener?) { + if (listener == null) return + logger.d("Register listener") + val registered = callbacks.register(listener) + if (registered && callbackCount.getAndIncrement() == 0) { + // If the first listener, start the collection job. This will also take + // care of notifying the listener of the initial state. + logger.d("Starting collection") + collectionJob = + lifecycleScope.launch(bgContext) { + combine( + homeControlsComponentInteractor.panelComponent, + controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen, + ) { panelComponent, allowTrivialControls -> + callbacks.notifyAllCallbacks(panelComponent, allowTrivialControls) + } + .launchIn(this) + } + } else if (registered) { + // If not the first listener, notify the listener of the current value immediately. + listener.notify( + homeControlsComponentInteractor.panelComponent.value, + controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value, + ) + } + } + + override fun unregisterListenerForCurrentUser(listener: IOnControlsSettingsChangeListener?) { + if (listener == null) return + logger.d("Unregister listener") + if (callbacks.unregister(listener) && callbackCount.decrementAndGet() == 0) { + logger.d("Cancelling collection due to unregister") + collectionJob?.cancel() + collectionJob = null + } + } + + private companion object { + const val TAG = "HomeControlsRemoteServiceBinder" + } + + private fun IOnControlsSettingsChangeListener.notify( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + try { + onControlsSettingsChanged(panelComponent, allowTrivialControlsOnLockscreen) + } catch (e: RemoteException) { + Log.e(TAG, "Error notifying callback", e) + } + } + + private fun RemoteCallbackList<IOnControlsSettingsChangeListener>.notifyAllCallbacks( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + val itemCount = beginBroadcast() + try { + for (i in 0 until itemCount) { + getBroadcastItem(i).notify(panelComponent, allowTrivialControlsOnLockscreen) + } + } finally { + finishBroadcast() + } + } + + @AssistedFactory + interface Factory { + fun create(lifecycleOwner: LifecycleOwner): HomeControlsRemoteServiceBinder + } +} 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 000000000000..ca255fda9e89 --- /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/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt index 20341389b75d..31bd70897f24 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt @@ -14,15 +14,9 @@ * limitations under the License. */ -package com.android.systemui.dreams.homecontrols.domain.interactor +package com.android.systemui.dreams.homecontrols.system.domain.interactor -import android.annotation.SuppressLint -import android.app.DreamManager import android.content.ComponentName -import android.os.PowerManager -import android.os.UserHandle -import com.android.systemui.common.domain.interactor.PackageChangeInteractor -import com.android.systemui.common.shared.model.PackageChangeModel import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController @@ -32,25 +26,16 @@ 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 com.android.systemui.util.kotlin.pairwiseBy -import com.android.systemui.util.kotlin.sample -import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject -import kotlin.math.abs -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -65,11 +50,7 @@ constructor( controlsComponent: ControlsComponent, authorizedPanelsRepository: AuthorizedPanelsRepository, userRepository: UserRepository, - private val packageChangeInteractor: PackageChangeInteractor, - private val systemClock: SystemClock, - private val powerManager: PowerManager, - private val dreamManager: DreamManager, - @Background private val bgScope: CoroutineScope + @Background private val bgScope: CoroutineScope, ) { private val controlsListingController: ControlsListingController? = controlsComponent.getControlsListingController().getOrNull() @@ -108,10 +89,7 @@ constructor( /** Gets all panels which are available and authorized by the user */ private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> = - combine( - allAvailableServices(), - allAuthorizedPanels, - ) { serviceInfos, authorizedPanels -> + combine(allAvailableServices(), allAuthorizedPanels) { serviceInfos, authorizedPanels -> serviceInfos.mapNotNull { val panelActivity = it.panelActivity if (it.componentName.packageName in authorizedPanels && panelActivity != null) { @@ -123,10 +101,7 @@ constructor( } val panelComponent: StateFlow<ComponentName?> = - combine( - allAvailableAndAuthorizedPanels, - selectedPanel, - ) { panels, selected -> + combine(allAvailableAndAuthorizedPanels, selectedPanel) { panels, selected -> val item = panels.firstOrNull { it.componentName == selected?.componentName } ?: panels.firstOrNull() @@ -134,67 +109,8 @@ constructor( } .stateIn(bgScope, SharingStarted.Eagerly, null) - private val taskFragmentFinished = - MutableSharedFlow<Long>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - - fun onDreamEndUnexpectedly() { - powerManager.userActivity( - systemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_OTHER, - PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS, - ) - taskFragmentFinished.tryEmit(systemClock.currentTimeMillis()) - } - - /** - * Monitors if the current home panel package is updated and causes the dream to finish, and - * attempts to restart the dream in this case. - */ - @SuppressLint("MissingPermission") - suspend fun monitorUpdatesAndRestart() { - taskFragmentFinished.resetReplayCache() - panelComponent - .flatMapLatest { component -> - if (component == null) return@flatMapLatest emptyFlow() - packageChangeInteractor.packageChanged(UserHandle.CURRENT, component.packageName) - } - .filter { it.isUpdate() } - // Wait for an UpdatedStarted - UpdateFinished pair to ensure the update has finished. - .pairwiseBy(::validateUpdatePair) - .filterNotNull() - .sample(taskFragmentFinished, ::Pair) - .filter { (updateStarted, lastFinishedTimestamp) -> - abs(updateStarted.timeMillis - lastFinishedTimestamp) <= - MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds - } - .collect { dreamManager.startDream() } - } - private data class PanelComponent( val componentName: ComponentName, val panelActivity: ComponentName, ) - - companion object { - /** - * The maximum delay between a package update **starting** and the task fragment finishing - * which causes us to correlate the package update as the cause of the task fragment - * finishing. - */ - val MAX_UPDATE_CORRELATION_DELAY = 500.milliseconds - } } - -private fun PackageChangeModel.isUpdate() = - this is PackageChangeModel.UpdateStarted || this is PackageChangeModel.UpdateFinished - -private fun validateUpdatePair( - updateStarted: PackageChangeModel, - updateFinished: PackageChangeModel -): PackageChangeModel.UpdateStarted? = - when { - !updateStarted.isSamePackage(updateFinished) -> null - updateStarted !is PackageChangeModel.UpdateStarted -> null - updateFinished !is PackageChangeModel.UpdateFinished -> null - else -> updateStarted - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt new file mode 100644 index 000000000000..f469d745ef73 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt @@ -0,0 +1,40 @@ +/* + * 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.dreams.homecontrols.shared.IHomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener + +/** Fake binder implementation of [IHomeControlsRemoteProxy] */ +class FakeIHomeControlsRemoteProxyBinder : IHomeControlsRemoteProxy.Stub() { + val callbacks = mutableSetOf<IOnControlsSettingsChangeListener>() + + override fun registerListenerForCurrentUser(callback: IOnControlsSettingsChangeListener) { + callbacks.add(callback) + } + + override fun unregisterListenerForCurrentUser(callback: IOnControlsSettingsChangeListener) { + callbacks.remove(callback) + } + + fun notifyCallbacks(component: ComponentName, allowTrivialControlsOnLockscreen: Boolean) { + for (callback in callbacks.toSet()) { + callback.onControlsSettingsChanged(component, allowTrivialControlsOnLockscreen) + } + } +} 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 000000000000..58ebbf4c2d9e --- /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 000000000000..c85c8884e6f3 --- /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/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt index 7dc5434c595e..f8ea022be0d8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt @@ -13,26 +13,21 @@ * 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 +package com.android.systemui.dreams.homecontrols.shared.model -class DreamServiceDelegateImpl @Inject constructor() : DreamServiceDelegate { - override fun getActivity(dreamService: DreamService): Activity { - return dreamService.activity - } +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull - override fun finish(dreamService: DreamService) { - dreamService.finish() - } +class FakeHomeControlsDataSource : HomeControlsDataSource { - override fun wakeUp(dreamService: DreamService) { - dreamService.wakeUp() - } + private val _componentInfo = MutableStateFlow<HomeControlsComponentInfo?>(null) + + override val componentInfo: Flow<HomeControlsComponentInfo> + get() = _componentInfo.filterNotNull() - override fun redirectWake(dreamService: DreamService): Boolean { - return dreamService.redirectWake + 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 000000000000..942216b6a6cc --- /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() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt index 7d7841f0da5b..a1182a178ead 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt @@ -13,21 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.dreams.homecontrols +package com.android.systemui.dreams.homecontrols.system.domain.interactor -import android.os.powerManager -import android.service.dream.dreamManager -import com.android.systemui.common.domain.interactor.packageChangeInteractor 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 -import com.android.systemui.util.time.fakeSystemClock val Kosmos.homeControlsComponentInteractor by Kosmos.Fixture { @@ -37,10 +32,6 @@ val Kosmos.homeControlsComponentInteractor by authorizedPanelsRepository = authorizedPanelsRepository, userRepository = fakeUserRepository, bgScope = applicationCoroutineScope, - systemClock = fakeSystemClock, - powerManager = powerManager, - dreamManager = dreamManager, - packageChangeInteractor = packageChangeInteractor, ) } |