blob: 5533063baa545f78fd523aecac0d39d9762455ef [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.permission3.cts
import android.app.Instrumentation
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.UiAutomation
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Context.RECEIVER_EXPORTED
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.EXTRA_STATUS
import android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE
import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID
import android.content.pm.PackageInstaller.STATUS_SUCCESS
import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager
import android.content.res.Resources
import android.os.PersistableBundle
import android.os.SystemClock
import android.provider.DeviceConfig
import android.provider.Settings
import android.text.Html
import android.util.Log
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.StaleObjectException
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import com.android.compatibility.common.util.DisableAnimationRule
import com.android.compatibility.common.util.FreezeRotationRule
import com.android.compatibility.common.util.SystemUtil.runShellCommand
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import com.android.compatibility.common.util.UiAutomatorUtils2
import com.android.modules.utils.build.SdkLevel
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Rule
abstract class BasePermissionTest {
companion object {
private const val TAG = "BasePermissionTest"
private const val INSTALL_ACTION_CALLBACK = "BasePermissionTest.install_callback"
const val APK_DIRECTORY = "/data/local/tmp/cts/permission3"
const val QUICK_CHECK_TIMEOUT_MILLIS = 100L
const val IDLE_TIMEOUT_MILLIS: Long = 1000
const val UNEXPECTED_TIMEOUT_MILLIS = 1000
const val TIMEOUT_MILLIS: Long = 20000
const val PACKAGE_INSTALLER_TIMEOUT = 60000L
@JvmStatic
protected val instrumentation: Instrumentation =
InstrumentationRegistry.getInstrumentation()
@JvmStatic
protected val context: Context = instrumentation.context
@JvmStatic
protected val uiAutomation: UiAutomation = instrumentation.uiAutomation
@JvmStatic
protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
@JvmStatic
protected val packageManager: PackageManager = context.packageManager
private val packageInstaller = packageManager.packageInstaller
@JvmStatic
private val mPermissionControllerResources: Resources = context.createPackageContext(
context.packageManager.permissionControllerPackageName, 0).resources
@JvmStatic
protected val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
@JvmStatic
protected val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
@JvmStatic
protected val isAutomotive =
packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
}
@get:Rule
val disableAnimationRule = DisableAnimationRule()
@get:Rule
val freezeRotationRule = FreezeRotationRule()
var activityScenario: ActivityScenario<StartForFutureActivity>? = null
data class SessionResult(val status: Int?)
/** If a status was received the value of the status, otherwise null */
private var installSessionResult = LinkedBlockingQueue<SessionResult>()
private val installSessionResultReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID)
val msg = intent.getStringExtra(EXTRA_STATUS_MESSAGE)
Log.d(TAG, "status: $status, msg: $msg")
installSessionResult.offer(SessionResult(status))
}
}
private var screenTimeoutBeforeTest: Long = 0L
@Before
fun setUp() {
runWithShellPermissionIdentity {
screenTimeoutBeforeTest = Settings.System.getLong(
context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT
)
Settings.System.putLong(
context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 1800000L
)
}
uiDevice.wakeUp()
runShellCommand(instrumentation, "wm dismiss-keyguard")
uiDevice.findObject(By.text("Close"))?.click()
}
@Before
fun registerInstallSessionResultReceiver() {
context.registerReceiver(
installSessionResultReceiver, IntentFilter(INSTALL_ACTION_CALLBACK), RECEIVER_EXPORTED)
}
@After
fun unregisterInstallSessionResultReceiver() {
try {
context.unregisterReceiver(installSessionResultReceiver)
} catch (ignored: IllegalArgumentException) {}
}
@After
fun tearDown() {
runWithShellPermissionIdentity {
Settings.System.putLong(
context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT,
screenTimeoutBeforeTest
)
}
try {
activityScenario?.close()
} catch (e: NullPointerException) {
// ignore
}
pressHome()
}
protected fun setDeviceConfigPrivacyProperty(
propertyName: String,
value: String,
) {
runWithShellPermissionIdentity(instrumentation.uiAutomation) {
val valueWasSet =
DeviceConfig.setProperty(
DeviceConfig.NAMESPACE_PRIVACY,
/* name = */ propertyName,
/* value = */ value,
/* makeDefault = */ false)
check(valueWasSet) { "Could not set $propertyName to $value" }
}
}
protected fun getPermissionControllerString(res: String, vararg formatArgs: Any): Pattern {
val textWithHtml = mPermissionControllerResources.getString(
mPermissionControllerResources.getIdentifier(
res, "string", "com.android.permissioncontroller"), *formatArgs)
val textWithoutHtml = Html.fromHtml(textWithHtml, 0).toString()
return Pattern.compile(Pattern.quote(textWithoutHtml),
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE)
}
protected fun getPermissionControllerResString(res: String): String? {
try {
return mPermissionControllerResources.getString(
mPermissionControllerResources.getIdentifier(
res, "string", "com.android.permissioncontroller"))
} catch (e: Resources.NotFoundException) {
return null
}
}
protected fun byAnyText(vararg texts: String?): BySelector {
var regex = ""
for (text in texts) {
if (text != null) {
regex = regex + Pattern.quote(text) + "|"
}
}
if (regex.endsWith("|")) {
regex = regex.dropLast(1)
}
return By.text(Pattern.compile(regex, Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE))
}
protected fun installPackage(
apkPath: String,
reinstall: Boolean = false,
grantRuntimePermissions: Boolean = false,
expectSuccess: Boolean = true,
installSource: String? = null
) {
val output = runShellCommand(
"pm install${if (SdkLevel.isAtLeastU()) " --bypass-low-target-sdk-block" else ""} " +
"${if (reinstall) " -r" else ""}${if (grantRuntimePermissions) " -g"
else ""}${if (installSource != null) " -i $installSource" else ""} $apkPath"
).trim()
if (expectSuccess) {
assertEquals("Success", output)
} else {
assertNotEquals("Success", output)
}
}
protected fun installPackageViaSession(
apkName: String,
appMetadata: PersistableBundle? = null,
packageSource: Int? = null
) {
val (sessionId, session) = createPackageInstallerSession(packageSource)
runWithShellPermissionIdentity {
writePackageInstallerSession(session, apkName)
if (appMetadata != null) {
setAppMetadata(session, appMetadata)
}
commitPackageInstallerSession(session)
// No need to click installer UI here due to running in shell permission identity and
// not needing user interaciton to complete install. Install should have succeeded.
val result = getInstallSessionResult()
assertThat(result.status).isEqualTo(STATUS_SUCCESS)
}
}
protected fun uninstallPackage(packageName: String, requireSuccess: Boolean = true) {
val output = runShellCommand("pm uninstall $packageName").trim()
if (requireSuccess) {
assertEquals("Success", output)
}
}
protected fun waitFindObject(selector: BySelector): UiObject2 {
waitForIdle()
return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObject(selector, t) })!!
}
protected fun waitFindObject(selector: BySelector, timeoutMillis: Long): UiObject2 {
waitForIdle()
return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObject(selector, t) },
timeoutMillis)!!
}
protected fun waitFindObjectOrNull(selector: BySelector): UiObject2? {
waitForIdle()
return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObjectOrNull(selector, t) })
}
protected fun waitFindObjectOrNull(selector: BySelector, timeoutMillis: Long): UiObject2? {
waitForIdle()
return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObjectOrNull(selector, t) },
timeoutMillis)
}
private fun findObjectWithRetry(
automatorMethod: (timeoutMillis: Long) -> UiObject2?,
timeoutMillis: Long = 20_000L
): UiObject2? {
waitForIdle()
val startTime = SystemClock.elapsedRealtime()
return try {
automatorMethod(timeoutMillis)
} catch (e: StaleObjectException) {
val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime)
if (remainingTime <= 0) {
throw e
}
automatorMethod(remainingTime)
}
}
protected fun click(selector: BySelector, timeoutMillis: Long = 20_000) {
waitFindObject(selector, timeoutMillis).click()
waitForIdle()
}
protected fun findView(selector: BySelector, expected: Boolean) {
val timeoutMs = if (expected) {
10000L
} else {
1000L
}
val exception = try {
waitFindObject(selector, timeoutMs)
null
} catch (e: Exception) {
e
}
Assert.assertTrue("Expected to find view: $expected", (exception == null) == expected)
}
protected fun clickPermissionControllerUi(selector: BySelector, timeoutMillis: Long = 20_000) {
click(selector.pkg(context.packageManager.permissionControllerPackageName), timeoutMillis)
}
protected fun pressBack() {
uiDevice.pressBack()
waitForIdle()
}
protected fun pressHome() {
uiDevice.pressHome()
waitForIdle()
}
protected fun pressDPadDown() {
uiDevice.pressDPadDown()
waitForIdle()
}
protected fun waitForIdle() = uiAutomation.waitForIdle(IDLE_TIMEOUT_MILLIS, TIMEOUT_MILLIS)
protected fun startActivityForFuture(
intent: Intent
): CompletableFuture<Instrumentation.ActivityResult> =
CompletableFuture<Instrumentation.ActivityResult>().also {
activityScenario = ActivityScenario.launch(
StartForFutureActivity::class.java).onActivity { activity ->
activity.startActivityForFuture(intent, it)
}
}
open fun enableComponent(component: ComponentName) {
packageManager.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP)
}
open fun disableComponent(component: ComponentName) {
packageManager.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP)
}
private fun createPackageInstallerSession(
packageSource: Int? = null
): Pair<Int, PackageInstaller.Session> {
// Create session
val sessionParam = SessionParams(SessionParams.MODE_FULL_INSTALL)
if (packageSource != null) {
sessionParam.setPackageSource(packageSource)
}
val sessionId = packageInstaller.createSession(sessionParam)
val session = packageInstaller.openSession(sessionId)!!
return Pair(sessionId, session)
}
private fun writePackageInstallerSession(session: PackageInstaller.Session, apkName: String) {
val apkFile = File(APK_DIRECTORY, apkName)
// Write data to session
apkFile.inputStream().use { fileOnDisk ->
session
.openWrite(/* name= */ apkName, /* offsetBytes= */ 0, /* lengthBytes= */ -1)
.use { sessionFile -> fileOnDisk.copyTo(sessionFile) }
}
}
private fun commitPackageInstallerSession(session: PackageInstaller.Session) {
// PendingIntent that triggers a INSTALL_ACTION_CALLBACK broadcast that gets received by
// installSessionResultReceiver when install actions occur with this session
val installActionPendingIntent =
PendingIntent.getBroadcast(
context,
0,
Intent(INSTALL_ACTION_CALLBACK).setPackage(context.packageName),
FLAG_UPDATE_CURRENT or FLAG_MUTABLE)
session.commit(installActionPendingIntent.intentSender)
}
private fun setAppMetadata(session: PackageInstaller.Session, data: PersistableBundle) {
try {
session.setAppMetadata(data)
} catch (e: Exception) {
session.abandon()
throw e
}
}
/** Wait for session's install result and return it */
private fun getInstallSessionResult(timeout: Long = PACKAGE_INSTALLER_TIMEOUT): SessionResult {
return installSessionResult.poll(timeout, TimeUnit.MILLISECONDS)
?: SessionResult(null /* status */)
}
}