Loading core/java/android/service/controls/ControlsProviderService.java +14 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,20 @@ public abstract class ControlsProviderService extends Service { public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService"; /** * Manifest metadata to show a custom embedded activity as part of device controls. * * The value of this metadata must be the {@link ComponentName} as a string of an activity in * the same package that will be launched as part of a TaskView. * * The activity must be exported, enabled and protected by * {@link Manifest.permission.BIND_CONTROLS}. * * @hide */ public static final String META_DATA_PANEL_ACTIVITY = "android.service.controls.META_DATA_PANEL_ACTIVITY"; /** * @hide */ Loading packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt +106 −2 Original line number Diff line number Diff line Loading @@ -16,16 +16,120 @@ package com.android.systemui.controls import android.Manifest import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.UserHandle import android.service.controls.ControlsProviderService import androidx.annotation.WorkerThread import com.android.settingslib.applications.DefaultAppInfo import java.util.Objects class ControlsServiceInfo( context: Context, private val context: Context, val serviceInfo: ServiceInfo ) : DefaultAppInfo( context, context.packageManager, context.userId, serviceInfo.componentName ) { private val _panelActivity: ComponentName? init { val metadata = serviceInfo.metaData ?.getString(ControlsProviderService.META_DATA_PANEL_ACTIVITY) ?: "" val unflatenned = ComponentName.unflattenFromString(metadata) if (unflatenned != null && unflatenned.packageName == componentName.packageName) { _panelActivity = unflatenned } else { _panelActivity = null } } /** * Component name of an activity that will be shown embedded in the device controls space * instead of using the controls rendered by SystemUI. * * The activity must be in the same package, exported, enabled and protected by the * [Manifest.permission.BIND_CONTROLS] permission. */ var panelActivity: ComponentName? = null private set private var resolved: Boolean = false @WorkerThread fun resolvePanelActivity() { if (resolved) return resolved = true panelActivity = _panelActivity?.let { val resolveInfos = mPm.queryIntentActivitiesAsUser( Intent().setComponent(it), PackageManager.ResolveInfoFlags.of( MATCH_DIRECT_BOOT_AWARE.toLong() or MATCH_DIRECT_BOOT_UNAWARE.toLong() ), UserHandle.of(userId) ) if (resolveInfos.isNotEmpty() && verifyResolveInfo(resolveInfos[0])) { it } else { null } } } /** * Verifies that the panel activity is enabled, exported and protected by the correct * permission. This last check is to prevent apps from forgetting to protect the activity, as * they won't be able to see the panel until they do. */ @WorkerThread private fun verifyResolveInfo(resolveInfo: ResolveInfo): Boolean { return resolveInfo.activityInfo?.let { it.permission == Manifest.permission.BIND_CONTROLS && it.exported && isComponentActuallyEnabled(it) } ?: false } @WorkerThread private fun isComponentActuallyEnabled(activityInfo: ActivityInfo): Boolean { return when (mPm.getComponentEnabledSetting(activityInfo.componentName)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> activityInfo.enabled else -> false } } override fun equals(other: Any?): Boolean { return other is ControlsServiceInfo && userId == other.userId && componentName == other.componentName && panelActivity == other.panelActivity } override fun hashCode(): Int { return Objects.hash(userId, componentName, panelActivity) } fun copy(): ControlsServiceInfo { return ControlsServiceInfo(context, serviceInfo).also { it.panelActivity = this.panelActivity } } override fun toString(): String { return """ ControlsServiceInfo(serviceInfo=$serviceInfo, panelActivity=$panelActivity, resolved=$resolved) """.trimIndent() } } No newline at end of file packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt +38 −22 Original line number Diff line number Diff line Loading @@ -18,17 +18,23 @@ package com.android.systemui.controls.management import android.content.ComponentName import android.content.Context import android.content.pm.ServiceInfo import android.os.UserHandle import android.service.controls.ControlsProviderService import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.applications.ServiceListing import com.android.settingslib.widget.CandidateInfo import com.android.systemui.Dumpable import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.settings.UserTracker import com.android.systemui.util.asIndenting import com.android.systemui.util.indentIfPossible import java.io.PrintWriter import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject Loading Loading @@ -57,16 +63,19 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( private val context: Context, @Background private val backgroundExecutor: Executor, private val serviceListingBuilder: (Context) -> ServiceListing, userTracker: UserTracker ) : ControlsListingController { private val userTracker: UserTracker, dumpManager: DumpManager, featureFlags: FeatureFlags ) : ControlsListingController, Dumpable { @Inject constructor(context: Context, executor: Executor, userTracker: UserTracker): this( context, executor, ::createServiceListing, userTracker ) constructor( context: Context, @Background executor: Executor, userTracker: UserTracker, dumpManager: DumpManager, featureFlags: FeatureFlags ) : this(context, executor, ::createServiceListing, userTracker, dumpManager, featureFlags) private var serviceListing = serviceListingBuilder(context) // All operations in background thread Loading @@ -76,27 +85,25 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( private const val TAG = "ControlsListingControllerImpl" } private var availableComponents = emptySet<ComponentName>() private var availableServices = emptyList<ServiceInfo>() private var availableServices = emptyList<ControlsServiceInfo>() private var userChangeInProgress = AtomicInteger(0) override var currentUserId = userTracker.userId private set private val serviceListingCallback = ServiceListing.Callback { val newServices = it.toList() val newComponents = newServices.mapTo(mutableSetOf<ComponentName>(), { s -> s.getComponentName() }) backgroundExecutor.execute { if (userChangeInProgress.get() > 0) return@execute if (!newComponents.equals(availableComponents)) { Log.d(TAG, "ServiceConfig reloaded, count: ${newComponents.size}") availableComponents = newComponents Log.d(TAG, "ServiceConfig reloaded, count: ${it.size}") val newServices = it.map { ControlsServiceInfo(userTracker.userContext, it) } if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { newServices.forEach(ControlsServiceInfo::resolvePanelActivity) } if (newServices != availableServices) { availableServices = newServices val currentServices = getCurrentServices() callbacks.forEach { it.onServicesUpdated(currentServices) it.onServicesUpdated(getCurrentServices()) } } } Loading @@ -104,6 +111,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( init { Log.d(TAG, "Initializing") dumpManager.registerDumpable(TAG, this) serviceListing.addCallback(serviceListingCallback) serviceListing.setListening(true) serviceListing.reload() Loading Loading @@ -165,7 +173,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( * [ControlsProviderService] */ override fun getCurrentServices(): List<ControlsServiceInfo> = availableServices.map { ControlsServiceInfo(context, it) } availableServices.map(ControlsServiceInfo::copy) /** * Get the localized label for the component. Loading @@ -174,7 +182,15 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( * @return a label as returned by [CandidateInfo.loadLabel] or `null`. */ override fun getAppLabel(name: ComponentName): CharSequence? { return getCurrentServices().firstOrNull { it.componentName == name } return availableServices.firstOrNull { it.componentName == name } ?.loadLabel() } override fun dump(writer: PrintWriter, args: Array<out String>) { writer.println("ControlsListingController:") writer.asIndenting().indentIfPossible { println("Callbacks: $callbacks") println("Services: ${getCurrentServices()}") } } } packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt +334 −17 Original line number Diff line number Diff line Loading @@ -16,27 +16,42 @@ package com.android.systemui.controls.management import android.Manifest import android.content.ComponentName import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.Bundle import android.os.UserHandle import android.service.controls.ControlsProviderService import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.settingslib.applications.ServiceListing import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.USE_APP_PANELS 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.argThat import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock Loading @@ -51,10 +66,8 @@ import java.util.concurrent.Executor class ControlsListingControllerImplTest : SysuiTestCase() { companion object { private const val TEST_LABEL = "TEST_LABEL" private const val TEST_PERMISSION = "permission" fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() fun <T> any(): T = Mockito.any<T>() private const val FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong() } @Mock Loading @@ -63,15 +76,17 @@ class ControlsListingControllerImplTest : SysuiTestCase() { private lateinit var mockCallback: ControlsListingController.ControlsListingCallback @Mock private lateinit var mockCallbackOther: ControlsListingController.ControlsListingCallback @Mock private lateinit var serviceInfo: ServiceInfo @Mock private lateinit var serviceInfo2: ServiceInfo @Mock(stubOnly = true) private lateinit var userTracker: UserTracker @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var featureFlags: FeatureFlags private var componentName = ComponentName("pkg1", "class1") private var componentName2 = ComponentName("pkg2", "class2") private var componentName = ComponentName("pkg", "class1") private var activityName = ComponentName("pkg", "activity") private val executor = FakeExecutor(FakeSystemClock()) Loading @@ -87,9 +102,15 @@ class ControlsListingControllerImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) `when`(serviceInfo.componentName).thenReturn(componentName) `when`(serviceInfo2.componentName).thenReturn(componentName2) `when`(userTracker.userId).thenReturn(user) `when`(userTracker.userContext).thenReturn(context) // Return disabled by default `when`(packageManager.getComponentEnabledSetting(any())) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED) mContext.setMockPackageManager(packageManager) // Return true by default, we'll test the false path `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(true) val wrapper = object : ContextWrapper(mContext) { override fun createContextAsUser(user: UserHandle, flags: Int): Context { Loading @@ -97,7 +118,14 @@ class ControlsListingControllerImplTest : SysuiTestCase() { } } controller = ControlsListingControllerImpl(wrapper, executor, { mockSL }, userTracker) controller = ControlsListingControllerImpl( wrapper, executor, { mockSL }, userTracker, dumpManager, featureFlags ) verify(mockSL).addCallback(capture(serviceListingCallbackCaptor)) } Loading @@ -123,9 +151,16 @@ class ControlsListingControllerImplTest : SysuiTestCase() { Unit } `when`(mockServiceListing.reload()).then { callback?.onServicesReloaded(listOf(serviceInfo)) callback?.onServicesReloaded(listOf(ServiceInfo(componentName))) } ControlsListingControllerImpl(mContext, exec, { mockServiceListing }, userTracker) ControlsListingControllerImpl( mContext, exec, { mockServiceListing }, userTracker, dumpManager, featureFlags ) } @Test Loading @@ -148,7 +183,7 @@ class ControlsListingControllerImplTest : SysuiTestCase() { @Test fun testCallbackGetsList() { val list = listOf(serviceInfo) val list = listOf(ServiceInfo(componentName)) controller.addCallback(mockCallback) controller.addCallback(mockCallbackOther) Loading Loading @@ -188,6 +223,8 @@ class ControlsListingControllerImplTest : SysuiTestCase() { @Test fun testChangeUserSendsCorrectServiceUpdate() { val serviceInfo = ServiceInfo(componentName) val list = listOf(serviceInfo) controller.addCallback(mockCallback) Loading Loading @@ -223,4 +260,284 @@ class ControlsListingControllerImplTest : SysuiTestCase() { verify(mockCallback).onServicesUpdated(capture(captor)) assertEquals(0, captor.value.size) } @Test fun test_nullPanelActivity() { val list = listOf(ServiceInfo(componentName)) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testNoActivity_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityWithoutPermission_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) setUpQueryResult(listOf(ActivityInfo(activityName))) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityPermissionNotExported_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) setUpQueryResult(listOf( ActivityInfo(activityName, permission = Manifest.permission.BIND_CONTROLS) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDisabled_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) setUpQueryResult(listOf( ActivityInfo( activityName, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityEnabled_correctPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED) setUpQueryResult(listOf( ActivityInfo( activityName, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertEquals(activityName, controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDefaultEnabled_correctPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = true, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertEquals(activityName, controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDefaultDisabled_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = false, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDefaultEnabled_flagDisabled_nullPanel() { `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(false) val serviceInfo = ServiceInfo( componentName, activityName, ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = true, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDifferentPackage_nullPanel() { val serviceInfo = ServiceInfo( componentName, ComponentName("other_package", "cls") ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = true, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } private fun ServiceInfo( componentName: ComponentName, panelActivityComponentName: ComponentName? = null ): ServiceInfo { return ServiceInfo().apply { packageName = componentName.packageName name = componentName.className panelActivityComponentName?.let { metaData = Bundle().apply { putString( ControlsProviderService.META_DATA_PANEL_ACTIVITY, it.flattenToShortString() ) } } } } private fun ActivityInfo( componentName: ComponentName, exported: Boolean = false, enabled: Boolean = true, permission: String? = null ): ActivityInfo { return ActivityInfo().apply { packageName = componentName.packageName name = componentName.className this.permission = permission this.exported = exported this.enabled = enabled } } private fun setUpQueryResult(infos: List<ActivityInfo>) { `when`( packageManager.queryIntentActivitiesAsUser( argThat(IntentMatcher(activityName)), argThat(FlagsMatcher(FLAGS)), eq(UserHandle.of(user)) ) ).thenReturn(infos.map { ResolveInfo().apply { activityInfo = it } }) } private class IntentMatcher( private val componentName: ComponentName ) : ArgumentMatcher<Intent> { override fun matches(argument: Intent?): Boolean { return argument?.component == componentName } } private class FlagsMatcher( private val flags: Long ) : ArgumentMatcher<PackageManager.ResolveInfoFlags> { override fun matches(argument: PackageManager.ResolveInfoFlags?): Boolean { return flags == argument?.value } } } Loading
core/java/android/service/controls/ControlsProviderService.java +14 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,20 @@ public abstract class ControlsProviderService extends Service { public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService"; /** * Manifest metadata to show a custom embedded activity as part of device controls. * * The value of this metadata must be the {@link ComponentName} as a string of an activity in * the same package that will be launched as part of a TaskView. * * The activity must be exported, enabled and protected by * {@link Manifest.permission.BIND_CONTROLS}. * * @hide */ public static final String META_DATA_PANEL_ACTIVITY = "android.service.controls.META_DATA_PANEL_ACTIVITY"; /** * @hide */ Loading
packages/SystemUI/src/com/android/systemui/controls/ControlsServiceInfo.kt +106 −2 Original line number Diff line number Diff line Loading @@ -16,16 +16,120 @@ package com.android.systemui.controls import android.Manifest import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.UserHandle import android.service.controls.ControlsProviderService import androidx.annotation.WorkerThread import com.android.settingslib.applications.DefaultAppInfo import java.util.Objects class ControlsServiceInfo( context: Context, private val context: Context, val serviceInfo: ServiceInfo ) : DefaultAppInfo( context, context.packageManager, context.userId, serviceInfo.componentName ) { private val _panelActivity: ComponentName? init { val metadata = serviceInfo.metaData ?.getString(ControlsProviderService.META_DATA_PANEL_ACTIVITY) ?: "" val unflatenned = ComponentName.unflattenFromString(metadata) if (unflatenned != null && unflatenned.packageName == componentName.packageName) { _panelActivity = unflatenned } else { _panelActivity = null } } /** * Component name of an activity that will be shown embedded in the device controls space * instead of using the controls rendered by SystemUI. * * The activity must be in the same package, exported, enabled and protected by the * [Manifest.permission.BIND_CONTROLS] permission. */ var panelActivity: ComponentName? = null private set private var resolved: Boolean = false @WorkerThread fun resolvePanelActivity() { if (resolved) return resolved = true panelActivity = _panelActivity?.let { val resolveInfos = mPm.queryIntentActivitiesAsUser( Intent().setComponent(it), PackageManager.ResolveInfoFlags.of( MATCH_DIRECT_BOOT_AWARE.toLong() or MATCH_DIRECT_BOOT_UNAWARE.toLong() ), UserHandle.of(userId) ) if (resolveInfos.isNotEmpty() && verifyResolveInfo(resolveInfos[0])) { it } else { null } } } /** * Verifies that the panel activity is enabled, exported and protected by the correct * permission. This last check is to prevent apps from forgetting to protect the activity, as * they won't be able to see the panel until they do. */ @WorkerThread private fun verifyResolveInfo(resolveInfo: ResolveInfo): Boolean { return resolveInfo.activityInfo?.let { it.permission == Manifest.permission.BIND_CONTROLS && it.exported && isComponentActuallyEnabled(it) } ?: false } @WorkerThread private fun isComponentActuallyEnabled(activityInfo: ActivityInfo): Boolean { return when (mPm.getComponentEnabledSetting(activityInfo.componentName)) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> activityInfo.enabled else -> false } } override fun equals(other: Any?): Boolean { return other is ControlsServiceInfo && userId == other.userId && componentName == other.componentName && panelActivity == other.panelActivity } override fun hashCode(): Int { return Objects.hash(userId, componentName, panelActivity) } fun copy(): ControlsServiceInfo { return ControlsServiceInfo(context, serviceInfo).also { it.panelActivity = this.panelActivity } } override fun toString(): String { return """ ControlsServiceInfo(serviceInfo=$serviceInfo, panelActivity=$panelActivity, resolved=$resolved) """.trimIndent() } } No newline at end of file
packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt +38 −22 Original line number Diff line number Diff line Loading @@ -18,17 +18,23 @@ package com.android.systemui.controls.management import android.content.ComponentName import android.content.Context import android.content.pm.ServiceInfo import android.os.UserHandle import android.service.controls.ControlsProviderService import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.applications.ServiceListing import com.android.settingslib.widget.CandidateInfo import com.android.systemui.Dumpable import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.settings.UserTracker import com.android.systemui.util.asIndenting import com.android.systemui.util.indentIfPossible import java.io.PrintWriter import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject Loading Loading @@ -57,16 +63,19 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( private val context: Context, @Background private val backgroundExecutor: Executor, private val serviceListingBuilder: (Context) -> ServiceListing, userTracker: UserTracker ) : ControlsListingController { private val userTracker: UserTracker, dumpManager: DumpManager, featureFlags: FeatureFlags ) : ControlsListingController, Dumpable { @Inject constructor(context: Context, executor: Executor, userTracker: UserTracker): this( context, executor, ::createServiceListing, userTracker ) constructor( context: Context, @Background executor: Executor, userTracker: UserTracker, dumpManager: DumpManager, featureFlags: FeatureFlags ) : this(context, executor, ::createServiceListing, userTracker, dumpManager, featureFlags) private var serviceListing = serviceListingBuilder(context) // All operations in background thread Loading @@ -76,27 +85,25 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( private const val TAG = "ControlsListingControllerImpl" } private var availableComponents = emptySet<ComponentName>() private var availableServices = emptyList<ServiceInfo>() private var availableServices = emptyList<ControlsServiceInfo>() private var userChangeInProgress = AtomicInteger(0) override var currentUserId = userTracker.userId private set private val serviceListingCallback = ServiceListing.Callback { val newServices = it.toList() val newComponents = newServices.mapTo(mutableSetOf<ComponentName>(), { s -> s.getComponentName() }) backgroundExecutor.execute { if (userChangeInProgress.get() > 0) return@execute if (!newComponents.equals(availableComponents)) { Log.d(TAG, "ServiceConfig reloaded, count: ${newComponents.size}") availableComponents = newComponents Log.d(TAG, "ServiceConfig reloaded, count: ${it.size}") val newServices = it.map { ControlsServiceInfo(userTracker.userContext, it) } if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { newServices.forEach(ControlsServiceInfo::resolvePanelActivity) } if (newServices != availableServices) { availableServices = newServices val currentServices = getCurrentServices() callbacks.forEach { it.onServicesUpdated(currentServices) it.onServicesUpdated(getCurrentServices()) } } } Loading @@ -104,6 +111,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( init { Log.d(TAG, "Initializing") dumpManager.registerDumpable(TAG, this) serviceListing.addCallback(serviceListingCallback) serviceListing.setListening(true) serviceListing.reload() Loading Loading @@ -165,7 +173,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( * [ControlsProviderService] */ override fun getCurrentServices(): List<ControlsServiceInfo> = availableServices.map { ControlsServiceInfo(context, it) } availableServices.map(ControlsServiceInfo::copy) /** * Get the localized label for the component. Loading @@ -174,7 +182,15 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( * @return a label as returned by [CandidateInfo.loadLabel] or `null`. */ override fun getAppLabel(name: ComponentName): CharSequence? { return getCurrentServices().firstOrNull { it.componentName == name } return availableServices.firstOrNull { it.componentName == name } ?.loadLabel() } override fun dump(writer: PrintWriter, args: Array<out String>) { writer.println("ControlsListingController:") writer.asIndenting().indentIfPossible { println("Callbacks: $callbacks") println("Services: ${getCurrentServices()}") } } }
packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt +334 −17 Original line number Diff line number Diff line Loading @@ -16,27 +16,42 @@ package com.android.systemui.controls.management import android.Manifest import android.content.ComponentName import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo import android.os.Bundle import android.os.UserHandle import android.service.controls.ControlsProviderService import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.settingslib.applications.ServiceListing import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.USE_APP_PANELS 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.argThat import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock Loading @@ -51,10 +66,8 @@ import java.util.concurrent.Executor class ControlsListingControllerImplTest : SysuiTestCase() { companion object { private const val TEST_LABEL = "TEST_LABEL" private const val TEST_PERMISSION = "permission" fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() fun <T> any(): T = Mockito.any<T>() private const val FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong() } @Mock Loading @@ -63,15 +76,17 @@ class ControlsListingControllerImplTest : SysuiTestCase() { private lateinit var mockCallback: ControlsListingController.ControlsListingCallback @Mock private lateinit var mockCallbackOther: ControlsListingController.ControlsListingCallback @Mock private lateinit var serviceInfo: ServiceInfo @Mock private lateinit var serviceInfo2: ServiceInfo @Mock(stubOnly = true) private lateinit var userTracker: UserTracker @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var featureFlags: FeatureFlags private var componentName = ComponentName("pkg1", "class1") private var componentName2 = ComponentName("pkg2", "class2") private var componentName = ComponentName("pkg", "class1") private var activityName = ComponentName("pkg", "activity") private val executor = FakeExecutor(FakeSystemClock()) Loading @@ -87,9 +102,15 @@ class ControlsListingControllerImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) `when`(serviceInfo.componentName).thenReturn(componentName) `when`(serviceInfo2.componentName).thenReturn(componentName2) `when`(userTracker.userId).thenReturn(user) `when`(userTracker.userContext).thenReturn(context) // Return disabled by default `when`(packageManager.getComponentEnabledSetting(any())) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DISABLED) mContext.setMockPackageManager(packageManager) // Return true by default, we'll test the false path `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(true) val wrapper = object : ContextWrapper(mContext) { override fun createContextAsUser(user: UserHandle, flags: Int): Context { Loading @@ -97,7 +118,14 @@ class ControlsListingControllerImplTest : SysuiTestCase() { } } controller = ControlsListingControllerImpl(wrapper, executor, { mockSL }, userTracker) controller = ControlsListingControllerImpl( wrapper, executor, { mockSL }, userTracker, dumpManager, featureFlags ) verify(mockSL).addCallback(capture(serviceListingCallbackCaptor)) } Loading @@ -123,9 +151,16 @@ class ControlsListingControllerImplTest : SysuiTestCase() { Unit } `when`(mockServiceListing.reload()).then { callback?.onServicesReloaded(listOf(serviceInfo)) callback?.onServicesReloaded(listOf(ServiceInfo(componentName))) } ControlsListingControllerImpl(mContext, exec, { mockServiceListing }, userTracker) ControlsListingControllerImpl( mContext, exec, { mockServiceListing }, userTracker, dumpManager, featureFlags ) } @Test Loading @@ -148,7 +183,7 @@ class ControlsListingControllerImplTest : SysuiTestCase() { @Test fun testCallbackGetsList() { val list = listOf(serviceInfo) val list = listOf(ServiceInfo(componentName)) controller.addCallback(mockCallback) controller.addCallback(mockCallbackOther) Loading Loading @@ -188,6 +223,8 @@ class ControlsListingControllerImplTest : SysuiTestCase() { @Test fun testChangeUserSendsCorrectServiceUpdate() { val serviceInfo = ServiceInfo(componentName) val list = listOf(serviceInfo) controller.addCallback(mockCallback) Loading Loading @@ -223,4 +260,284 @@ class ControlsListingControllerImplTest : SysuiTestCase() { verify(mockCallback).onServicesUpdated(capture(captor)) assertEquals(0, captor.value.size) } @Test fun test_nullPanelActivity() { val list = listOf(ServiceInfo(componentName)) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testNoActivity_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityWithoutPermission_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) setUpQueryResult(listOf(ActivityInfo(activityName))) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityPermissionNotExported_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) setUpQueryResult(listOf( ActivityInfo(activityName, permission = Manifest.permission.BIND_CONTROLS) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDisabled_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) setUpQueryResult(listOf( ActivityInfo( activityName, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityEnabled_correctPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED) setUpQueryResult(listOf( ActivityInfo( activityName, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertEquals(activityName, controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDefaultEnabled_correctPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = true, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertEquals(activityName, controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDefaultDisabled_nullPanel() { val serviceInfo = ServiceInfo( componentName, activityName ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = false, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDefaultEnabled_flagDisabled_nullPanel() { `when`(featureFlags.isEnabled(USE_APP_PANELS)).thenReturn(false) val serviceInfo = ServiceInfo( componentName, activityName, ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = true, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } @Test fun testActivityDifferentPackage_nullPanel() { val serviceInfo = ServiceInfo( componentName, ComponentName("other_package", "cls") ) `when`(packageManager.getComponentEnabledSetting(eq(activityName))) .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) setUpQueryResult(listOf( ActivityInfo( activityName, enabled = true, exported = true, permission = Manifest.permission.BIND_CONTROLS ) )) val list = listOf(serviceInfo) serviceListingCallbackCaptor.value.onServicesReloaded(list) executor.runAllReady() assertNull(controller.getCurrentServices()[0].panelActivity) } private fun ServiceInfo( componentName: ComponentName, panelActivityComponentName: ComponentName? = null ): ServiceInfo { return ServiceInfo().apply { packageName = componentName.packageName name = componentName.className panelActivityComponentName?.let { metaData = Bundle().apply { putString( ControlsProviderService.META_DATA_PANEL_ACTIVITY, it.flattenToShortString() ) } } } } private fun ActivityInfo( componentName: ComponentName, exported: Boolean = false, enabled: Boolean = true, permission: String? = null ): ActivityInfo { return ActivityInfo().apply { packageName = componentName.packageName name = componentName.className this.permission = permission this.exported = exported this.enabled = enabled } } private fun setUpQueryResult(infos: List<ActivityInfo>) { `when`( packageManager.queryIntentActivitiesAsUser( argThat(IntentMatcher(activityName)), argThat(FlagsMatcher(FLAGS)), eq(UserHandle.of(user)) ) ).thenReturn(infos.map { ResolveInfo().apply { activityInfo = it } }) } private class IntentMatcher( private val componentName: ComponentName ) : ArgumentMatcher<Intent> { override fun matches(argument: Intent?): Boolean { return argument?.component == componentName } } private class FlagsMatcher( private val flags: Long ) : ArgumentMatcher<PackageManager.ResolveInfoFlags> { override fun matches(argument: PackageManager.ResolveInfoFlags?): Boolean { return flags == argument?.value } } }