diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt index dbe301df8e780..860149d8acee5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt @@ -28,12 +28,13 @@ import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.UserHandle import android.service.controls.ControlsProviderService +import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import com.android.settingslib.applications.DefaultAppInfo import com.android.systemui.R import java.util.Objects -class ControlsServiceInfo( +open class ControlsServiceInfo( private val context: Context, val serviceInfo: ServiceInfo ) : DefaultAppInfo( @@ -64,7 +65,7 @@ class ControlsServiceInfo( * [R.array.config_controlsPreferredPackages] can declare activities for use as a panel. */ var panelActivity: ComponentName? = null - private set + protected set private var resolved: Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index ce82af7c8f99f..700f4f62029f5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -123,16 +123,13 @@ class ControlsControllerImpl @Inject constructor ( userChanging = false } - private val userTrackerCallback = object : UserTracker.Callback { - override fun onUserChanged(newUser: Int, userContext: Context) { - userChanging = true - val newUserHandle = UserHandle.of(newUser) - if (currentUser == newUserHandle) { - userChanging = false - return - } - setValuesForUser(newUserHandle) + override fun changeUser(newUser: UserHandle) { + userChanging = true + if (currentUser == newUser) { + userChanging = false + return } + setValuesForUser(newUser) } @VisibleForTesting @@ -233,7 +230,6 @@ class ControlsControllerImpl @Inject constructor ( dumpManager.registerDumpable(javaClass.name, this) resetFavorites() userChanging = false - userTracker.addCallback(userTrackerCallback, executor) context.registerReceiver( restoreFinishedReceiver, IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), @@ -245,7 +241,6 @@ class ControlsControllerImpl @Inject constructor ( } fun destroy() { - userTracker.removeCallback(userTrackerCallback) context.unregisterReceiver(restoreFinishedReceiver) listingController.removeCallback(listingCallback) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt new file mode 100644 index 0000000000000..3f20c26dadb9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/StartControlsStartableModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 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.controls.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.controls.start.ControlsStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +abstract class StartControlsStartableModule { + @Binds + @IntoMap + @ClassKey(ControlsStartable::class) + abstract fun bindFeature(impl: ControlsStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt new file mode 100644 index 0000000000000..9d99253de741d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2023 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.controls.start + +import android.content.Context +import android.content.res.Resources +import android.os.UserHandle +import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.ui.SelectedItem +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Started with SystemUI to perform early operations for device controls subsystem (only if enabled) + * + * In particular, it will perform the following: + * * If there is no preferred selection for provider and at least one of the preferred packages + * provides a panel, it will select the first one that does. + * * If the preferred selection provides a panel, it will bind to that service (to reduce latency on + * displaying the panel). + * + * It will also perform those operations on user change. + */ +@SysUISingleton +class ControlsStartable +@Inject +constructor( + @Main private val resources: Resources, + @Background private val executor: Executor, + private val controlsComponent: ControlsComponent, + private val userTracker: UserTracker +) : CoreStartable { + + // These two controllers can only be accessed after `start` method once we've checked if the + // feature is enabled + private val controlsController: ControlsController + get() = controlsComponent.getControlsController().get() + + private val controlsListingController: ControlsListingController + get() = controlsComponent.getControlsListingController().get() + + private val userTrackerCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + controlsController.changeUser(UserHandle.of(newUser)) + startForUser() + } + } + + override fun start() { + if (!controlsComponent.isEnabled()) { + // Controls is disabled, we don't need this anymore + return + } + startForUser() + userTracker.addCallback(userTrackerCallback, executor) + } + + private fun startForUser() { + selectDefaultPanelIfNecessary() + bindToPanel() + } + + private fun selectDefaultPanelIfNecessary() { + val currentSelection = controlsController.getPreferredSelection() + if (currentSelection == SelectedItem.EMPTY_SELECTION) { + val availableServices = controlsListingController.getCurrentServices() + val panels = availableServices.filter { it.panelActivity != null } + resources + .getStringArray(R.array.config_controlsPreferredPackages) + // Looking for the first element in the string array such that there is one package + // that has a panel. It will return null if there are no packages in the array, + // or if no packages in the array have a panel associated with it. + .firstNotNullOfOrNull { name -> + panels.firstOrNull { it.componentName.packageName == name } + } + ?.let { info -> + controlsController.setPreferredSelection( + SelectedItem.PanelItem(info.loadLabel(), info.componentName) + ) + } + } + } + + private fun bindToPanel() { + val currentSelection = controlsController.getPreferredSelection() + val panels = + controlsListingController.getCurrentServices().filter { it.panelActivity != null } + if ( + currentSelection is SelectedItem.PanelItem && + panels.firstOrNull { it.componentName == currentSelection.componentName } != null + ) { + controlsController.bindComponentForPanel(currentSelection.componentName) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index caa0e96cf5fc6..b5a1f4593a38c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -27,6 +27,7 @@ import com.android.systemui.accessibility.WindowMagnification import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.UdfpsOverlay import com.android.systemui.clipboardoverlay.ClipboardListener +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 @@ -67,7 +68,10 @@ import dagger.multibindings.IntoMap /** * Collection of {@link CoreStartable}s that should be run on AOSP. */ -@Module(includes = [MultiUserUtilsModule::class]) +@Module(includes = [ + MultiUserUtilsModule::class, + StartControlsStartableModule::class +]) abstract class SystemUICoreStartableModule { /** Inject into AuthController. */ @Binds diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index d54babfbdc0e9..e35b2a384bd0c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -103,8 +103,6 @@ class ControlsControllerImplTest : SysuiTestCase() { private lateinit var controlLoadCallbackCaptor2: ArgumentCaptor - @Captor - private lateinit var userTrackerCallbackCaptor: ArgumentCaptor @Captor private lateinit var listingCallbackCaptor: ArgumentCaptor @@ -178,10 +176,6 @@ class ControlsControllerImplTest : SysuiTestCase() { ) controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper - verify(userTracker).addCallback( - capture(userTrackerCallbackCaptor), any() - ) - verify(listingController).addCallback(capture(listingCallbackCaptor)) } @@ -539,7 +533,7 @@ class ControlsControllerImplTest : SysuiTestCase() { reset(persistenceWrapper) - userTrackerCallbackCaptor.value.onUserChanged(otherUser, mContext) + controller.changeUser(UserHandle.of(otherUser)) verify(persistenceWrapper).changeFileAndBackupManager(any(), any()) verify(persistenceWrapper).readFavorites() diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt new file mode 100644 index 0000000000000..7ecaca6c36d01 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2023 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.controls.start + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.controller.ControlsController +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.ui.SelectedItem +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +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.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ControlsStartableTest : SysuiTestCase() { + + @Mock private lateinit var controlsController: ControlsController + @Mock private lateinit var controlsListingController: ControlsListingController + @Mock private lateinit var userTracker: UserTracker + + private lateinit var fakeExecutor: FakeExecutor + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf() + ) + + fakeExecutor = FakeExecutor(FakeSystemClock()) + } + + @Test + fun testDisabledNothingIsCalled() { + createStartable(enabled = false).start() + + verifyZeroInteractions(controlsController, controlsListingController, userTracker) + } + + @Test + fun testNoPreferredPackagesNoDefaultSelected_noNewSelection() { + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController, never()).setPreferredSelection(any()) + } + + @Test + fun testPreferredPackagesNotInstalled_noNewSelection() { + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE_PANEL) + ) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList()) + + createStartable(enabled = true).start() + + verify(controlsController, never()).setPreferredSelection(any()) + } + + @Test + fun testPreferredPackageNotPanel_noNewSelection() { + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE_PANEL) + ) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController, never()).setPreferredSelection(any()) + } + + @Test + fun testExistingSelection_noNewSelection() { + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE_PANEL) + ) + `when`(controlsController.getPreferredSelection()) + .thenReturn(mock()) + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController, never()).setPreferredSelection(any()) + } + + @Test + fun testPanelAdded() { + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE_PANEL) + ) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController).setPreferredSelection(listings[0].toPanelItem()) + } + + @Test + fun testMultiplePreferredOnlyOnePanel_panelAdded() { + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf("other_package", TEST_PACKAGE_PANEL) + ) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + val listings = + listOf( + ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true), + ControlsServiceInfo(ComponentName("other_package", "cls"), "non panel", false) + ) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController).setPreferredSelection(listings[0].toPanelItem()) + } + + @Test + fun testMultiplePreferredMultiplePanels_firstPreferredAdded() { + context.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE_PANEL, "other_package") + ) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + val listings = + listOf( + ControlsServiceInfo(ComponentName("other_package", "cls"), "panel", true), + ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true) + ) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + + createStartable(enabled = true).start() + + verify(controlsController).setPreferredSelection(listings[1].toPanelItem()) + } + + @Test + fun testPreferredSelectionIsPanel_bindOnStart() { + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem()) + + createStartable(enabled = true).start() + + verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL) + } + + @Test + fun testPreferredSelectionPanel_listingNoPanel_notBind() { + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + `when`(controlsController.getPreferredSelection()) + .thenReturn(SelectedItem.PanelItem("panel", TEST_COMPONENT_PANEL)) + + createStartable(enabled = true).start() + + verify(controlsController, never()).bindComponentForPanel(any()) + } + + @Test + fun testNotPanelSelection_noBind() { + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false)) + `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) + + createStartable(enabled = true).start() + + verify(controlsController, never()).bindComponentForPanel(any()) + } + + private fun createStartable(enabled: Boolean): ControlsStartable { + val component: ControlsComponent = + mock() { + `when`(isEnabled()).thenReturn(enabled) + if (enabled) { + `when`(getControlsController()).thenReturn(Optional.of(controlsController)) + `when`(getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + } else { + `when`(getControlsController()).thenReturn(Optional.empty()) + `when`(getControlsListingController()).thenReturn(Optional.empty()) + } + } + return ControlsStartable(context.resources, fakeExecutor, component, userTracker) + } + + 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 fun ControlsServiceInfo.toPanelItem(): SelectedItem.PanelItem { + if (panelActivity == null) { + throw IllegalArgumentException("$this is not a panel") + } + return SelectedItem.PanelItem(loadLabel(), componentName) + } + + 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") + } +}