| /* |
| * 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 android.os.cts |
| |
| import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING |
| import android.app.Instrumentation |
| import android.content.Context |
| import android.content.Intent |
| import android.content.Intent.ACTION_AUTO_REVOKE_PERMISSIONS |
| import android.content.Intent.FLAG_ACTIVITY_NEW_TASK |
| import android.content.pm.PackageManager |
| import android.content.pm.PackageManager.PERMISSION_DENIED |
| import android.content.pm.PackageManager.PERMISSION_GRANTED |
| import android.content.res.Resources |
| import android.net.Uri |
| import android.os.Build |
| import android.os.UserHandle |
| import android.platform.test.annotations.AppModeFull |
| import android.provider.DeviceConfig |
| import android.safetycenter.SafetyCenterIssue |
| import android.safetycenter.SafetyCenterManager |
| import android.support.test.uiautomator.By |
| import android.support.test.uiautomator.BySelector |
| import android.support.test.uiautomator.UiObject2 |
| import android.support.test.uiautomator.UiObjectNotFoundException |
| import android.view.accessibility.AccessibilityNodeInfo |
| import android.widget.Switch |
| import androidx.test.InstrumentationRegistry |
| import androidx.test.filters.SdkSuppress |
| import androidx.test.runner.AndroidJUnit4 |
| import com.android.compatibility.common.util.DeviceConfigStateChangerRule |
| import com.android.compatibility.common.util.DisableAnimationRule |
| import com.android.compatibility.common.util.FreezeRotationRule |
| import com.android.compatibility.common.util.MatcherUtils.hasTextThat |
| import com.android.compatibility.common.util.SystemUtil |
| import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity |
| import com.android.compatibility.common.util.SystemUtil.eventually |
| import com.android.compatibility.common.util.SystemUtil.getEventually |
| import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow |
| import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity |
| import com.android.compatibility.common.util.UI_ROOT |
| import com.android.compatibility.common.util.click |
| import com.android.compatibility.common.util.depthFirstSearch |
| import com.android.compatibility.common.util.uiDump |
| import com.android.modules.utils.build.SdkLevel |
| import com.android.safetycenter.internaldata.SafetyCenterIds |
| import com.android.safetycenter.internaldata.SafetyCenterIssueId |
| import com.android.safetycenter.internaldata.SafetyCenterIssueKey |
| import java.lang.reflect.Modifier |
| import java.util.concurrent.TimeUnit |
| import java.util.concurrent.atomic.AtomicReference |
| import java.util.regex.Pattern |
| import org.hamcrest.CoreMatchers.containsString |
| import org.hamcrest.CoreMatchers.containsStringIgnoringCase |
| import org.hamcrest.CoreMatchers.equalTo |
| import org.hamcrest.Matcher |
| import org.hamcrest.Matchers.greaterThan |
| import org.junit.After |
| import org.junit.Assert.assertEquals |
| import org.junit.Assert.assertFalse |
| import org.junit.Assert.assertThat |
| import org.junit.Assert.assertTrue |
| import org.junit.Assume.assumeFalse |
| import org.junit.Before |
| import org.junit.BeforeClass |
| import org.junit.Ignore |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| |
| private const val READ_CALENDAR = "android.permission.READ_CALENDAR" |
| private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT" |
| |
| /** |
| * Test for auto revoke |
| */ |
| @RunWith(AndroidJUnit4::class) |
| class AutoRevokeTest { |
| private val context: Context = InstrumentationRegistry.getTargetContext() |
| private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() |
| |
| private val mPermissionControllerResources: Resources = context.createPackageContext( |
| context.packageManager.permissionControllerPackageName, 0).resources |
| |
| private lateinit var supportedApkPath: String |
| private lateinit var supportedAppPackageName: String |
| private lateinit var preMinVersionApkPath: String |
| private lateinit var preMinVersionAppPackageName: String |
| |
| @Rule |
| @JvmField |
| val storeExactTimeRule = DeviceConfigStateChangerRule(context, |
| DeviceConfig.NAMESPACE_PERMISSIONS, STORE_EXACT_TIME_KEY, "true") |
| |
| companion object { |
| const val LOG_TAG = "AutoRevokeTest" |
| private const val STORE_EXACT_TIME_KEY = "permission_changes_store_exact_time" |
| private const val UNUSED_APPS_SOURCE_ID = "AndroidPermissionAutoRevoke" |
| private const val UNUSED_APPS_ISSUE_ID = "unused_apps_issue" |
| |
| @JvmStatic |
| @BeforeClass |
| fun beforeAllTests() { |
| runBootCompleteReceiver(InstrumentationRegistry.getTargetContext(), LOG_TAG) |
| } |
| } |
| |
| @get:Rule |
| val disableAnimationRule = DisableAnimationRule() |
| |
| @get:Rule |
| val freezeRotationRule = FreezeRotationRule() |
| |
| @Before |
| fun setup() { |
| // Collapse notifications |
| assertThat( |
| runShellCommandOrThrow("cmd statusbar collapse"), |
| equalTo("")) |
| |
| // Wake up the device |
| runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP") |
| if ("false".equals(runShellCommandOrThrow("cmd lock_settings get-disabled"))) { |
| // Unlock screen only when it's lock settings enabled to prevent showing "wallpaper |
| // picker" which may cover another UI elements on freeform window configuration. |
| runShellCommandOrThrow("input keyevent 82") |
| } |
| |
| if (isAutomotiveDevice()) { |
| supportedApkPath = APK_PATH_S_APP |
| supportedAppPackageName = APK_PACKAGE_NAME_S_APP |
| preMinVersionApkPath = APK_PATH_R_APP |
| preMinVersionAppPackageName = APK_PACKAGE_NAME_R_APP |
| } else { |
| supportedApkPath = APK_PATH_R_APP |
| supportedAppPackageName = APK_PACKAGE_NAME_R_APP |
| preMinVersionApkPath = APK_PATH_Q_APP |
| preMinVersionAppPackageName = APK_PACKAGE_NAME_Q_APP |
| } |
| } |
| |
| @After |
| fun cleanUp() { |
| goHome() |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| @Ignore("b/201545116") |
| fun testUnusedApp_getsPermissionRevoked() { |
| assumeFalse( |
| "Watch doesn't provide a unified way to check notifications. it depends on UX", |
| hasFeatureWatch()) |
| withUnusedThresholdMs(3L) { |
| withDummyApp { |
| // Setup |
| startAppAndAcceptPermission() |
| killDummyApp() |
| Thread.sleep(5) // wait longer than the unused threshold |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify |
| assertPermission(PERMISSION_DENIED) |
| |
| if (hasFeatureTV()) { |
| // Skip checking unused apps screen because it may be unavailable on TV |
| return |
| } |
| openUnusedAppsNotification() |
| |
| waitFindObject(By.text(supportedAppPackageName)) |
| waitFindObject(By.text("Calendar permission removed")) |
| goBack() |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testUnusedApp_uninstallApp() { |
| assumeFalse( |
| "Unused apps screen may be unavailable on TV", |
| hasFeatureTV()) |
| withUnusedThresholdMs(3L) { |
| withDummyAppNoUninstallAssertion { |
| // Setup |
| startAppAndAcceptPermission() |
| killDummyApp() |
| Thread.sleep(5) // wait longer than the unused threshold |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify |
| openUnusedAppsNotification() |
| waitFindObject(By.text(supportedAppPackageName)) |
| |
| assertTrue(isPackageInstalled(supportedAppPackageName)) |
| clickUninstallIcon() |
| clickUninstallOk() |
| |
| eventually { |
| assertFalse(isPackageInstalled(supportedAppPackageName)) |
| } |
| |
| goBack() |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S") |
| @Test |
| fun testUnusedApp_doesntGetSplitPermissionRevoked() { |
| assumeFalse( |
| "Auto doesn't support hibernation for pre-S apps", |
| isAutomotiveDevice()) |
| withUnusedThresholdMs(3L) { |
| withDummyApp(APK_PATH_R_APP, APK_PACKAGE_NAME_R_APP) { |
| // Setup |
| startApp(APK_PACKAGE_NAME_R_APP) |
| assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_R_APP, BLUETOOTH_CONNECT) |
| killDummyApp(APK_PACKAGE_NAME_R_APP) |
| Thread.sleep(500) |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify |
| assertPermission(PERMISSION_GRANTED, APK_PACKAGE_NAME_R_APP, BLUETOOTH_CONNECT) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testUsedApp_doesntGetPermissionRevoked() { |
| withUnusedThresholdMs(100_000L) { |
| withDummyApp { |
| // Setup |
| startApp() |
| clickPermissionAllow() |
| assertPermission(PERMISSION_GRANTED) |
| killDummyApp() |
| Thread.sleep(5) |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| Thread.sleep(1000) |
| |
| // Verify |
| assertPermission(PERMISSION_GRANTED) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testAppWithPermissionsChangedRecently_doesNotGetPermissionRevoked() { |
| val unusedThreshold = 15_000L |
| withUnusedThresholdMs(unusedThreshold) { |
| withDummyApp { |
| // Setup |
| // Ensure app is considered unused and then change permission |
| Thread.sleep(unusedThreshold) |
| goToPermissions() |
| click("Calendar") |
| click("Allow") |
| goBack() |
| goBack() |
| goBack() |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify that permission is not revoked because the permission was changed |
| // within the unused threshold even though the app itself is unused |
| assertPermission(PERMISSION_GRANTED) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testPermissionEventCleanupService_scrubsEvents() { |
| val unusedThreshold = 15_000L |
| withUnusedThresholdMs(unusedThreshold) { |
| withDummyApp { |
| // Setup |
| // Ensure app is considered unused |
| Thread.sleep(unusedThreshold) |
| goToPermissions() |
| click("Calendar") |
| click("Allow") |
| goBack() |
| goBack() |
| goBack() |
| // Run with threshold where events would be cleaned up |
| withUnusedThresholdMs(0) { |
| runPermissionEventCleanupJob(context) |
| Thread.sleep(3000L) |
| } |
| |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify that permission is revoked because there are no recent permission changes |
| assertPermission(PERMISSION_DENIED) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testPreMinAutoRevokeVersionUnusedApp_doesntGetPermissionRevoked() { |
| withUnusedThresholdMs(3L) { |
| withDummyApp(preMinVersionApkPath, preMinVersionAppPackageName) { |
| withDummyApp { |
| startApp(preMinVersionAppPackageName) |
| clickPermissionAllow() |
| assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName) |
| |
| killDummyApp(preMinVersionAppPackageName) |
| |
| startApp() |
| clickPermissionAllow() |
| assertPermission(PERMISSION_GRANTED) |
| |
| killDummyApp() |
| Thread.sleep(20) |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| Thread.sleep(500) |
| |
| // Verify |
| assertPermission(PERMISSION_DENIED) |
| assertPermission(PERMISSION_GRANTED, preMinVersionAppPackageName) |
| } |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testAutoRevoke_userAllowlisting() { |
| assumeFalse(context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) |
| withUnusedThresholdMs(4L) { |
| withDummyApp { |
| // Setup |
| startApp() |
| clickPermissionAllow() |
| assertAllowlistState(false) |
| |
| // Verify |
| waitFindObject(byTextIgnoreCase("Request allowlist")).click() |
| waitFindObject(byTextIgnoreCase("Permissions")).click() |
| val autoRevokeEnabledToggle = getAllowlistToggle() |
| assertTrue(autoRevokeEnabledToggle.isChecked()) |
| |
| // Grant allowlist |
| autoRevokeEnabledToggle.click() |
| eventually { |
| assertFalse(getAllowlistToggle().isChecked()) |
| } |
| |
| // Run |
| goBack() |
| goBack() |
| goBack() |
| runAppHibernationJob(context, LOG_TAG) |
| Thread.sleep(500L) |
| |
| // Verify |
| startApp() |
| assertAllowlistState(true) |
| assertPermission(PERMISSION_GRANTED) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testInstallGrants_notRevokedImmediately() { |
| withUnusedThresholdMs(TimeUnit.DAYS.toMillis(30)) { |
| withDummyApp { |
| // Setup |
| goToPermissions() |
| click("Calendar") |
| click("Allow") |
| goBack() |
| goBack() |
| goBack() |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| Thread.sleep(500) |
| |
| // Verify |
| assertPermission(PERMISSION_GRANTED) |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @Test |
| fun testAutoRevoke_allowlistingApis() { |
| withDummyApp { |
| val pm = context.packageManager |
| runWithShellPermissionIdentity { |
| assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName)) |
| } |
| |
| runWithShellPermissionIdentity { |
| assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, true)) |
| } |
| eventually { |
| runWithShellPermissionIdentity { |
| assertTrue(pm.isAutoRevokeWhitelisted(supportedAppPackageName)) |
| } |
| } |
| |
| runWithShellPermissionIdentity { |
| assertTrue(pm.setAutoRevokeWhitelisted(supportedAppPackageName, false)) |
| } |
| eventually { |
| runWithShellPermissionIdentity { |
| assertFalse(pm.isAutoRevokeWhitelisted(supportedAppPackageName)) |
| } |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu") |
| @Test |
| fun testAutoRevoke_showsUpInSafetyCenter() { |
| withSafetyCenterEnabled { |
| withUnusedThresholdMs(3L) { |
| withDummyApp { |
| startAppAndAcceptPermission() |
| |
| killDummyApp() |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Verify |
| val safetyCenterManager = |
| context.getSystemService(SafetyCenterManager::class.java)!! |
| eventually { |
| val issues = ArrayList<SafetyCenterIssue>() |
| runWithShellPermissionIdentity { |
| val safetyCenterData = safetyCenterManager!!.safetyCenterData |
| issues.addAll(safetyCenterData.issues) |
| } |
| val issueId = SafetyCenterIds.encodeToString( |
| SafetyCenterIssueId.newBuilder() |
| .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder() |
| .setSafetySourceId(UNUSED_APPS_SOURCE_ID) |
| .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID) |
| .setUserId(UserHandle.myUserId()) |
| .build()) |
| .setIssueTypeId(UNUSED_APPS_ISSUE_ID) |
| .build()) |
| assertTrue(issues.any { it.id == issueId }) |
| } |
| } |
| } |
| } |
| } |
| |
| @AppModeFull(reason = "Uses separate apps for testing") |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu") |
| @Test |
| fun testAutoRevoke_goToUnusedAppsPage_removesSafetyCenterIssue() { |
| withSafetyCenterEnabled { |
| withUnusedThresholdMs(3L) { |
| withDummyApp { |
| startAppAndAcceptPermission() |
| |
| killDummyApp() |
| |
| // Run |
| runAppHibernationJob(context, LOG_TAG) |
| |
| // Go to unused apps page |
| openUnusedAppsNotification() |
| waitFindObject(By.text(supportedAppPackageName)) |
| |
| // Verify |
| val safetyCenterManager = |
| context.getSystemService(SafetyCenterManager::class.java)!! |
| eventually { |
| val issues = ArrayList<SafetyCenterIssue>() |
| runWithShellPermissionIdentity { |
| val safetyCenterData = safetyCenterManager!!.safetyCenterData |
| issues.addAll(safetyCenterData.issues) |
| } |
| val issueId = SafetyCenterIds.encodeToString( |
| SafetyCenterIssueId.newBuilder() |
| .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder() |
| .setSafetySourceId(UNUSED_APPS_SOURCE_ID) |
| .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID) |
| .setUserId(UserHandle.myUserId()) |
| .build()) |
| .setIssueTypeId(UNUSED_APPS_ISSUE_ID) |
| .build()) |
| assertFalse(issues.any { it.id == issueId }) |
| } |
| } |
| } |
| } |
| } |
| |
| private fun isAutomotiveDevice(): Boolean { |
| return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) |
| } |
| |
| private fun installApp() { |
| installApk(supportedApkPath) |
| } |
| |
| private fun isPackageInstalled(packageName: String): Boolean { |
| val pm = context.packageManager |
| |
| return callWithShellPermissionIdentity { |
| try { |
| pm.getPackageInfo(packageName, 0) |
| true |
| } catch (e: PackageManager.NameNotFoundException) { |
| false |
| } |
| } |
| } |
| |
| private fun uninstallApp() { |
| uninstallApp(supportedAppPackageName) |
| } |
| |
| private fun startApp() { |
| startApp(supportedAppPackageName) |
| } |
| |
| private fun startAppAndAcceptPermission() { |
| startApp() |
| clickPermissionAllow() |
| assertPermission(PERMISSION_GRANTED) |
| } |
| |
| private fun goBack() { |
| runShellCommandOrThrow("input keyevent KEYCODE_BACK") |
| } |
| |
| private fun killDummyApp(pkg: String = supportedAppPackageName) { |
| if (!SdkLevel.isAtLeastS()) { |
| // Work around a race condition on R that killing the app process too fast after |
| // activity launch would result in a stale process record in LRU process list that |
| // sticks until next reboot. |
| Thread.sleep(5000) |
| } |
| assertThat( |
| runShellCommandOrThrow("am force-stop " + pkg), |
| equalTo("")) |
| awaitAppState(pkg, greaterThan(IMPORTANCE_TOP_SLEEPING)) |
| } |
| |
| private fun clickPermissionAllow() { |
| if (isAutomotiveDevice()) { |
| waitFindObject(By.text(Pattern.compile( |
| Pattern.quote(mPermissionControllerResources.getString( |
| mPermissionControllerResources.getIdentifier( |
| "grant_dialog_button_allow", "string", |
| "com.android.permissioncontroller"))), |
| Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE))).click() |
| } else { |
| waitFindObject(By.res("com.android.permissioncontroller:id/permission_allow_button")) |
| .click() |
| } |
| waitForIdle() |
| } |
| |
| private fun clickUninstallIcon() { |
| val rowSelector = By.text(supportedAppPackageName) |
| val rowItem = waitFindObject(rowSelector).parent.parent |
| |
| val uninstallSelector = if (isAutomotiveDevice()) { |
| By.res("com.android.permissioncontroller:id/car_ui_secondary_action") |
| } else { |
| By.desc("Uninstall or disable") |
| } |
| |
| rowItem.findObject(uninstallSelector).click() |
| } |
| |
| private fun clickUninstallOk() { |
| waitFindObject(By.text("OK")).click() |
| } |
| |
| private inline fun withDummyApp( |
| apk: String = supportedApkPath, |
| packageName: String = supportedAppPackageName, |
| action: () -> Unit |
| ) { |
| withApp(apk, packageName, action) |
| } |
| |
| private inline fun withDummyAppNoUninstallAssertion( |
| apk: String = supportedApkPath, |
| packageName: String = supportedAppPackageName, |
| action: () -> Unit |
| ) { |
| withAppNoUninstallAssertion(apk, packageName, action) |
| } |
| |
| private fun assertPermission( |
| state: Int, |
| packageName: String = supportedAppPackageName, |
| permission: String = READ_CALENDAR |
| ) { |
| assertPermission(packageName, permission, state) |
| } |
| |
| private fun goToPermissions(packageName: String = supportedAppPackageName) { |
| context.startActivity(Intent(ACTION_AUTO_REVOKE_PERMISSIONS) |
| .setData(Uri.fromParts("package", packageName, null)) |
| .addFlags(FLAG_ACTIVITY_NEW_TASK)) |
| |
| waitForIdle() |
| |
| click("Permissions") |
| } |
| |
| private fun click(label: String) { |
| try { |
| waitFindObject(byTextIgnoreCase(label)).click() |
| } catch (e: UiObjectNotFoundException) { |
| // waitFindObject sometimes fails to find UI that is present in the view hierarchy |
| // Increasing sleep to 2000 in waitForIdle() might be passed but no guarantee that the |
| // UI is fully displayed So Adding one more check without using the UiAutomator helps |
| // reduce false positives |
| waitFindNode(hasTextThat(containsStringIgnoringCase(label))).click() |
| } |
| waitForIdle() |
| } |
| |
| private fun assertAllowlistState(state: Boolean) { |
| assertThat( |
| waitFindObject(By.textStartsWith("Auto-revoke allowlisted: ")).text, |
| containsString(state.toString())) |
| } |
| |
| private fun getAllowlistToggle(): UiObject2 { |
| waitForIdle() |
| val parent = waitFindObject( |
| By.clickable(true) |
| .hasDescendant(By.textStartsWith("Remove permissions")) |
| .hasDescendant(By.clazz(Switch::class.java.name)) |
| ) |
| return parent.findObject(By.clazz(Switch::class.java.name)) |
| } |
| |
| private fun waitForIdle() { |
| instrumentation.uiAutomation.waitForIdle(1000, 10000) |
| Thread.sleep(500) |
| instrumentation.uiAutomation.waitForIdle(1000, 10000) |
| } |
| |
| private inline fun <T> eventually(crossinline action: () -> T): T { |
| val res = AtomicReference<T>() |
| SystemUtil.eventually { |
| res.set(action()) |
| } |
| return res.get() |
| } |
| |
| private fun waitFindObject(selector: BySelector): UiObject2 { |
| return waitFindObject(instrumentation.uiAutomation, selector) |
| } |
| } |
| |
| private fun permissionStateToString(state: Int): String { |
| return constToString<PackageManager>("PERMISSION_", state) |
| } |
| |
| /** |
| * For some reason waitFindObject sometimes fails to find UI that is present in the view hierarchy |
| */ |
| fun waitFindNode( |
| matcher: Matcher<AccessibilityNodeInfo>, |
| failMsg: String? = null, |
| timeoutMs: Long = 10_000 |
| ): AccessibilityNodeInfo { |
| return getEventually({ |
| val ui = UI_ROOT |
| ui.depthFirstSearch { node -> |
| matcher.matches(node) |
| }.assertNotNull { |
| buildString { |
| if (failMsg != null) { |
| appendLine(failMsg) |
| } |
| appendLine("No view found matching $matcher:\n\n${uiDump(ui)}") |
| } |
| } |
| }, timeoutMs) |
| } |
| |
| fun byTextIgnoreCase(txt: String): BySelector { |
| return By.text(Pattern.compile(txt, Pattern.CASE_INSENSITIVE)) |
| } |
| |
| fun waitForIdle() { |
| InstrumentationRegistry.getInstrumentation().uiAutomation.waitForIdle(1000, 10000) |
| } |
| |
| fun uninstallApp(packageName: String) { |
| assertThat(runShellCommandOrThrow("pm uninstall $packageName"), containsString("Success")) |
| } |
| |
| fun uninstallAppWithoutAssertion(packageName: String) { |
| runShellCommandOrThrow("pm uninstall $packageName") |
| } |
| |
| fun installApk(apk: String) { |
| assertThat(runShellCommandOrThrow("pm install -r $apk"), containsString("Success")) |
| } |
| |
| fun assertPermission(packageName: String, permissionName: String, state: Int) { |
| assertThat(permissionName, containsString("permission.")) |
| eventually { |
| runWithShellPermissionIdentity { |
| assertEquals( |
| permissionStateToString(state), |
| permissionStateToString( |
| InstrumentationRegistry.getTargetContext() |
| .packageManager |
| .checkPermission(permissionName, packageName))) |
| } |
| } |
| } |
| |
| inline fun <reified T> constToString(prefix: String, value: Int): String { |
| return T::class.java.declaredFields.filter { |
| Modifier.isStatic(it.modifiers) && it.name.startsWith(prefix) |
| }.map { |
| it.isAccessible = true |
| it.name to it.get(null) |
| }.find { (k, v) -> |
| v == value |
| }.assertNotNull { |
| "None of ${T::class.java.simpleName}.$prefix* == $value" |
| }.first |
| } |
| |
| inline fun <T> T?.assertNotNull(errorMsg: () -> String): T { |
| return if (this == null) throw AssertionError(errorMsg()) else this |
| } |