blob: c677f19f93e50b375c078d4b99068744ff00c108 [file] [log] [blame]
/*
* Copyright (C) 2020 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.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.R
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.Assert.assertTrue
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.`when`
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.concurrent.Executor
@SmallTest
@RunWith(AndroidTestingRunner::class)
class ControlsListingControllerImplTest : SysuiTestCase() {
companion object {
private const val FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or
PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong()
}
@Mock
private lateinit var mockSL: ServiceListing
@Mock
private lateinit var mockCallback: ControlsListingController.ControlsListingCallback
@Mock
private lateinit var mockCallbackOther: ControlsListingController.ControlsListingCallback
@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("pkg", "class1")
private var activityName = ComponentName("pkg", "activity")
private val executor = FakeExecutor(FakeSystemClock())
private lateinit var controller: ControlsListingControllerImpl
private var serviceListingCallbackCaptor =
ArgumentCaptor.forClass(ServiceListing.Callback::class.java)
private val user = mContext.userId
private val otherUser = user + 1
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
`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)
mContext.orCreateTestableResources
.addOverride(
R.array.config_controlsPreferredPackages,
arrayOf(componentName.packageName)
)
// 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 {
return baseContext
}
}
controller = ControlsListingControllerImpl(
wrapper,
executor,
{ mockSL },
userTracker,
dumpManager,
featureFlags
)
verify(mockSL).addCallback(capture(serviceListingCallbackCaptor))
}
@After
fun tearDown() {
executor.advanceClockToLast()
executor.runAllReady()
}
@Test
fun testInitialStateListening() {
verify(mockSL).setListening(true)
verify(mockSL).reload()
}
@Test
fun testImmediateListingReload_doesNotCrash() {
val exec = Executor { it.run() }
val mockServiceListing = mock(ServiceListing::class.java)
var callback: ServiceListing.Callback? = null
`when`(mockServiceListing.addCallback(any<ServiceListing.Callback>())).then {
callback = it.getArgument(0)
Unit
}
`when`(mockServiceListing.reload()).then {
callback?.onServicesReloaded(listOf(ServiceInfo(componentName)))
}
ControlsListingControllerImpl(
mContext,
exec,
{ mockServiceListing },
userTracker,
dumpManager,
featureFlags
)
}
@Test
fun testStartsOnUser() {
assertEquals(user, controller.currentUserId)
}
@Test
fun testCallbackCalledWhenAdded() {
controller.addCallback(mockCallback)
executor.runAllReady()
verify(mockCallback).onServicesUpdated(any())
reset(mockCallback)
controller.addCallback(mockCallbackOther)
executor.runAllReady()
verify(mockCallbackOther).onServicesUpdated(any())
verify(mockCallback, never()).onServicesUpdated(any())
}
@Test
fun testCallbackGetsList() {
val list = listOf(ServiceInfo(componentName))
controller.addCallback(mockCallback)
controller.addCallback(mockCallbackOther)
@Suppress("unchecked_cast")
val captor: ArgumentCaptor<List<ControlsServiceInfo>> =
ArgumentCaptor.forClass(List::class.java)
as ArgumentCaptor<List<ControlsServiceInfo>>
executor.runAllReady()
reset(mockCallback)
reset(mockCallbackOther)
serviceListingCallbackCaptor.value.onServicesReloaded(list)
executor.runAllReady()
verify(mockCallback).onServicesUpdated(capture(captor))
assertEquals(1, captor.value.size)
assertEquals(componentName.flattenToString(), captor.value[0].key)
verify(mockCallbackOther).onServicesUpdated(capture(captor))
assertEquals(1, captor.value.size)
assertEquals(componentName.flattenToString(), captor.value[0].key)
}
@Test
fun testChangeUser() {
controller.changeUser(UserHandle.of(otherUser))
executor.runAllReady()
assertEquals(otherUser, controller.currentUserId)
val inOrder = inOrder(mockSL)
inOrder.verify(mockSL).setListening(false)
inOrder.verify(mockSL).addCallback(any()) // We add a callback because we replaced the SL
inOrder.verify(mockSL).setListening(true)
inOrder.verify(mockSL).reload()
}
@Test
fun testChangeUserSendsCorrectServiceUpdate() {
val serviceInfo = ServiceInfo(componentName)
val list = listOf(serviceInfo)
controller.addCallback(mockCallback)
@Suppress("unchecked_cast")
val captor: ArgumentCaptor<List<ControlsServiceInfo>> =
ArgumentCaptor.forClass(List::class.java)
as ArgumentCaptor<List<ControlsServiceInfo>>
executor.runAllReady()
reset(mockCallback)
serviceListingCallbackCaptor.value.onServicesReloaded(list)
executor.runAllReady()
verify(mockCallback).onServicesUpdated(capture(captor))
assertEquals(1, captor.value.size)
reset(mockCallback)
reset(mockSL)
val updatedList = listOf(serviceInfo)
serviceListingCallbackCaptor.value.onServicesReloaded(updatedList)
controller.changeUser(UserHandle.of(otherUser))
executor.runAllReady()
assertEquals(otherUser, controller.currentUserId)
// this event should was triggered just before the user change, and should
// be ignored
verify(mockCallback, never()).onServicesUpdated(any())
serviceListingCallbackCaptor.value.onServicesReloaded(emptyList<ServiceInfo>())
executor.runAllReady()
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)
}
@Test
fun testPackageNotPreferred_nullPanel() {
mContext.orCreateTestableResources
.addOverride(R.array.config_controlsPreferredPackages, arrayOf<String>())
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()
assertNull(controller.getCurrentServices()[0].panelActivity)
}
@Test
fun testListingsNotModifiedByCallback() {
// This test checks that if the list passed to the callback is modified, it has no effect
// in the resulting services
val list = mutableListOf<ServiceInfo>()
serviceListingCallbackCaptor.value.onServicesReloaded(list)
list.add(ServiceInfo(ComponentName("a", "b")))
executor.runAllReady()
assertTrue(controller.getCurrentServices().isEmpty())
}
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
}
}
}