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