Merge changes I61922e99,I656b81fe into tm-qpr-dev

* changes:
  Add code for collecting panel activity component
  Add meta data for panel
This commit is contained in:
Fabian Kozynski
2022-11-10 14:25:02 +00:00
committed by Android (Google) Code Review
4 changed files with 492 additions and 41 deletions

View File

@@ -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
*/

View File

@@ -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()
}
}

View File

@@ -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
@@ -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
@@ -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())
}
}
}
@@ -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()
@@ -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.
@@ -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()}")
}
}
}

View File

@@ -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
@@ -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
@@ -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())
@@ -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 {
@@ -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))
}
@@ -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
@@ -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)
@@ -188,6 +223,8 @@ class ControlsListingControllerImplTest : SysuiTestCase() {
@Test
fun testChangeUserSendsCorrectServiceUpdate() {
val serviceInfo = ServiceInfo(componentName)
val list = listOf(serviceInfo)
controller.addCallback(mockCallback)
@@ -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
}
}
}