Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7515e83e authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Android (Google) Code Review
Browse files

Merge changes I61922e99,I656b81fe into tm-qpr-dev

* changes:
  Add code for collecting panel activity component
  Add meta data for panel
parents f5090690 42e27378
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -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
     */
+106 −2
Original line number Diff line number Diff line
@@ -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
+38 −22
Original line number Diff line number Diff line
@@ -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()}")
        }
    }
}
+334 −17
Original line number Diff line number Diff line
@@ -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
        }
    }
}