blob: ecdb30f5e84b6842d6d4376adfeb4ca0a60eaf9b [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.server.pm.test.override
import android.content.ComponentName
import android.content.Context
import android.content.pm.parsing.component.ParsedActivity
import android.os.Binder
import android.os.UserHandle
import android.util.ArrayMap
import com.android.server.pm.AppsFilter
import com.android.server.pm.ComponentResolver
import com.android.server.pm.PackageManagerService
import com.android.server.pm.PackageSetting
import com.android.server.pm.Settings
import com.android.server.pm.UserManagerService
import com.android.server.pm.parsing.pkg.AndroidPackage
import com.android.server.pm.parsing.pkg.PackageImpl
import com.android.server.pm.parsing.pkg.ParsedPackage
import com.android.server.pm.permission.PermissionManagerServiceInternal
import com.android.server.pm.test.override.PackageManagerComponentLabelIconOverrideTest.Companion.Params.AppType
import com.android.server.pm.test.override.R
import com.android.server.testutils.TestHandler
import com.android.server.testutils.mock
import com.android.server.testutils.mockThrowOnUnmocked
import com.android.server.testutils.spy
import com.android.server.testutils.whenever
import com.android.server.wm.ActivityTaskManagerInternal
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Mockito.any
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.intThat
import org.mockito.Mockito.never
import org.mockito.Mockito.same
import org.mockito.Mockito.verify
import org.testng.Assert.assertThrows
import java.io.File
@RunWith(Parameterized::class)
class PackageManagerComponentLabelIconOverrideTest {
companion object {
private const val VALID_PKG = "com.android.server.pm.test.override"
private const val SHARED_PKG = "com.android.server.pm.test.override.shared"
private const val INVALID_PKG = "com.android.server.pm.test.override.invalid"
private const val SEND_PENDING_BROADCAST = 1 // PackageManagerService.SEND_PENDING_BROADCAST
private const val DEFAULT_LABEL = "DefaultLabel"
private const val TEST_LABEL = "TestLabel"
private const val DEFAULT_ICON = R.drawable.black16x16
private const val TEST_ICON = R.drawable.white16x16
private const val COMPONENT_CLASS_NAME = ".TestComponent"
sealed class Result {
// Component label/icon changed, message sent to send broadcast
object Changed : Result()
// Component label/icon changed, message was pending, not re-sent
object ChangedWithoutNotify : Result()
// Component label/icon did not changed, was already equivalent
object NotChanged : Result()
// Updating label/icon encountered a specific exception
data class Exception(val type: Class<out java.lang.Exception>) : Result()
}
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun parameters() = arrayOf(
// Start with an array of the simplest known inputs and expected outputs
Params(VALID_PKG, AppType.SYSTEM_APP, Result.Changed),
Params(SHARED_PKG, AppType.SYSTEM_APP, Result.Changed),
Params(INVALID_PKG, AppType.SYSTEM_APP, SecurityException::class.java)
)
.flatMap { param ->
mutableListOf(param).apply {
if (param.result is Result.Changed) {
// For each param that would've succeeded, also verify that if a change
// happened, but a message was pending, another is not re-queued/reset
this += param.copy(result = Result.ChangedWithoutNotify)
// Also verify that when the component is already configured, no change
// is propagated
this += param.copy(result = Result.NotChanged)
}
// For all params, verify that an invalid component will cause an
// IllegalArgumentException, instead of result initially specified
this += param.copy(componentName = null,
result = Result.Exception(IllegalArgumentException::class.java))
// Also verify an updated system app variant, which should have the same
// result as a vanilla system app
this += param.copy(appType = AppType.UPDATED_SYSTEM_APP)
// Also verify a non-system app will cause a failure, since normal apps
// are not allowed to edit their label/icon
this += param.copy(appType = AppType.NORMAL_APP,
result = Result.Exception(SecurityException::class.java))
}
}
data class Params(
val pkgName: String,
private val appType: AppType,
val result: Result,
val componentName: ComponentName? = ComponentName(pkgName, COMPONENT_CLASS_NAME)
) {
constructor(pkgName: String, appType: AppType, exception: Class<out Exception>)
: this(pkgName, appType, Result.Exception(exception))
val expectedLabel = when (result) {
Result.Changed, Result.ChangedWithoutNotify, Result.NotChanged -> TEST_LABEL
is Result.Exception -> DEFAULT_LABEL
}
val expectedIcon = when (result) {
Result.Changed, Result.ChangedWithoutNotify, Result.NotChanged -> TEST_ICON
is Result.Exception -> DEFAULT_ICON
}
val isUpdatedSystemApp = appType == AppType.UPDATED_SYSTEM_APP
val isSystem = appType == AppType.SYSTEM_APP || isUpdatedSystemApp
override fun toString(): String {
val resultString = when (result) {
Result.Changed -> "Changed"
Result.ChangedWithoutNotify -> "ChangedWithoutNotify"
Result.NotChanged -> "NotChanged"
is Result.Exception -> result.type.simpleName
}
// Nicer formatting for the test method suffix
return "pkg=$pkgName, type=$appType, component=$componentName, result=$resultString"
}
enum class AppType { SYSTEM_APP, UPDATED_SYSTEM_APP, NORMAL_APP }
}
}
@Parameterized.Parameter(0)
lateinit var params: Params
private lateinit var testHandler: TestHandler
private lateinit var mockPendingBroadcasts: PackageManagerService.PendingPackageBroadcasts
private lateinit var mockPkg: AndroidPackage
private lateinit var mockPkgSetting: PackageSetting
private lateinit var service: PackageManagerService
private val userId = UserHandle.getCallingUserId()
private val userIdDifferent = userId + 1
@Before
fun setUpMocks() {
makeTestData()
testHandler = TestHandler(null)
if (params.result is Result.ChangedWithoutNotify) {
// Case where the handler already has a message and so another should not be sent.
// This case will verify that only 1 message exists, which is the one added here.
testHandler.sendEmptyMessage(SEND_PENDING_BROADCAST)
}
mockPendingBroadcasts = PackageManagerService.PendingPackageBroadcasts()
service = mockService()
}
@Test
fun updateComponentLabelIcon() {
fun runUpdate() {
service.updateComponentLabelIcon(params.componentName, TEST_LABEL, TEST_ICON, userId)
}
when (val result = params.result) {
Result.Changed, Result.ChangedWithoutNotify, Result.NotChanged -> {
runUpdate()
verify(mockPkgSetting).overrideNonLocalizedLabelAndIcon(params.componentName!!,
TEST_LABEL, TEST_ICON, userId)
}
is Result.Exception -> {
assertThrows(result.type) { runUpdate() }
verify(mockPkgSetting, never()).overrideNonLocalizedLabelAndIcon(
any<ComponentName>(), any(), anyInt(), anyInt())
}
}
}
@After
fun verifyExpectedResult() {
if (params.componentName != null) {
val activityInfo = service.getActivityInfo(params.componentName, 0, userId)
assertThat(activityInfo.nonLocalizedLabel).isEqualTo(params.expectedLabel)
assertThat(activityInfo.icon).isEqualTo(params.expectedIcon)
}
}
@After
fun verifyDifferentUserUnchanged() {
when (params.result) {
Result.Changed, Result.ChangedWithoutNotify -> {
val activityInfo = service.getActivityInfo(params.componentName, 0, userIdDifferent)
assertThat(activityInfo.nonLocalizedLabel).isEqualTo(DEFAULT_LABEL)
assertThat(activityInfo.icon).isEqualTo(DEFAULT_ICON)
}
Result.NotChanged, is Result.Exception -> {}
}.run { /*exhaust*/ }
}
@After
fun verifyHandlerHasMessage() {
when (params.result) {
is Result.Changed, is Result.ChangedWithoutNotify -> {
assertThat(testHandler.pendingMessages).hasSize(1)
assertThat(testHandler.pendingMessages.first().message.what)
.isEqualTo(SEND_PENDING_BROADCAST)
}
is Result.NotChanged, is Result.Exception -> {
assertThat(testHandler.pendingMessages).hasSize(0)
}
}.run { /*exhaust*/ }
}
@After
fun verifyPendingBroadcast() {
when (params.result) {
is Result.Changed, Result.ChangedWithoutNotify -> {
assertThat(mockPendingBroadcasts.get(userId, params.pkgName))
.containsExactly(params.componentName!!.className)
.inOrder()
}
is Result.NotChanged, is Result.Exception -> {
assertThat(mockPendingBroadcasts.get(userId, params.pkgName)).isNull()
}
}.run { /*exhaust*/ }
}
private fun makePkg(pkgName: String, block: ParsedPackage.() -> Unit = {}) =
PackageImpl.forTesting(pkgName)
.setEnabled(true)
.let { it.hideAsParsed() as ParsedPackage }
.setSystem(params.isSystem)
.apply(block)
.hideAsFinal()
private fun makePkgSetting(pkgName: String) = spy(PackageSetting(pkgName, null, File("/test"),
File("/test"), null, null, null, null, 0, 0, 0, 0, null, null, null)) {
this.pkgState.isUpdatedSystemApp = params.isUpdatedSystemApp
}
private fun makeTestData() {
mockPkg = makePkg(params.pkgName)
mockPkgSetting = makePkgSetting(params.pkgName)
if (params.result is Result.NotChanged) {
// If verifying no-op behavior, set the current setting to the test values
mockPkgSetting.overrideNonLocalizedLabelAndIcon(params.componentName!!, TEST_LABEL,
TEST_ICON, userId)
// Then clear the mock because the line above just incremented it
clearInvocations(mockPkgSetting)
}
}
private fun mockService(): PackageManagerService {
val mockedPkgs = mapOf(
// Must use the test app's UID so that PMS can match them when querying, since
// the static Binder.getCallingUid can't mocked as it's marked final
VALID_PKG to makePkg(VALID_PKG) { uid = Binder.getCallingUid() },
SHARED_PKG to makePkg(SHARED_PKG) { uid = Binder.getCallingUid() },
INVALID_PKG to makePkg(INVALID_PKG) { uid = Binder.getCallingUid() + 1 }
)
val mockedPkgSettings = mapOf(
VALID_PKG to makePkgSetting(VALID_PKG),
SHARED_PKG to makePkgSetting(SHARED_PKG),
INVALID_PKG to makePkgSetting(INVALID_PKG)
)
// Add pkgSetting under test so its attributes override the defaults added above
.plus(params.pkgName to mockPkgSetting)
val mockActivity: ParsedActivity = mock {
whenever(this.packageName) { params.pkgName }
whenever(this.nonLocalizedLabel) { DEFAULT_LABEL }
whenever(this.icon) { DEFAULT_ICON }
whenever(this.componentName) { params.componentName }
whenever(this.name) { params.componentName?.className }
whenever(this.isEnabled) { true }
whenever(this.isDirectBootAware) { params.isSystem }
}
val mockSettings = Settings(mockedPkgSettings)
val mockComponentResolver: ComponentResolver = mockThrowOnUnmocked {
params.componentName?.let {
whenever(this.componentExists(same(it))) { true }
whenever(this.getActivity(same(it))) { mockActivity }
}
}
val mockUserManagerService: UserManagerService = mockThrowOnUnmocked {
val matcher: (Int) -> Boolean = { it == userId || it == userIdDifferent }
whenever(this.exists(intThat(matcher))) { true }
whenever(this.isUserUnlockingOrUnlocked(intThat(matcher))) { true }
}
val mockPermissionManagerService: PermissionManagerServiceInternal = mockThrowOnUnmocked {
whenever(this.enforceCrossUserPermission(anyInt(), anyInt(), anyBoolean(), anyBoolean(),
anyString())) { }
}
val mockActivityTaskManager: ActivityTaskManagerInternal = mockThrowOnUnmocked {
whenever(this.isCallerRecents(anyInt())) { false }
}
val mockAppsFilter: AppsFilter = mockThrowOnUnmocked {
whenever(this.shouldFilterApplication(anyInt(), any<PackageSetting>(),
any<PackageSetting>(), anyInt())) { false }
}
val mockContext: Context = mockThrowOnUnmocked {
whenever(this.getString(
com.android.internal.R.string.config_overrideComponentUiPackage)) { VALID_PKG }
}
val mockInjector: PackageManagerService.Injector = mock {
whenever(this.lock) { Object() }
whenever(this.componentResolver) { mockComponentResolver }
whenever(this.userManagerService) { mockUserManagerService }
whenever(this.permissionManagerServiceInternal) { mockPermissionManagerService }
whenever(this.settings) { mockSettings }
whenever(this.activityTaskManagerInternal) { mockActivityTaskManager }
whenever(this.appsFilter) { mockAppsFilter }
whenever(this.context) { mockContext }
}
val testParams = PackageManagerService.TestParams().apply {
this.handler = testHandler
this.pendingPackageBroadcasts = mockPendingBroadcasts
this.resolveComponentName = ComponentName("android", ".Test")
this.packages = ArrayMap<String, AndroidPackage>().apply { putAll(mockedPkgs) }
}
return PackageManagerService(mockInjector, testParams)
}
}