diff options
Diffstat (limited to 'tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt')
-rw-r--r-- | tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt new file mode 100644 index 00000000000..485a4efe431 --- /dev/null +++ b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2021 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.hibernation.cts + +import android.app.Activity +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING +import android.app.Instrumentation +import android.app.UiAutomation +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Looper +import android.os.ParcelFileDescriptor +import android.os.Process +import android.provider.DeviceConfig +import android.support.test.uiautomator.By +import android.support.test.uiautomator.BySelector +import android.support.test.uiautomator.UiDevice +import android.support.test.uiautomator.UiObject2 +import android.support.test.uiautomator.UiScrollable +import android.support.test.uiautomator.UiSelector +import android.support.test.uiautomator.Until +import android.util.Log +import androidx.test.InstrumentationRegistry +import com.android.compatibility.common.util.ExceptionUtils.wrappingExceptions +import com.android.compatibility.common.util.LogcatInspector +import com.android.compatibility.common.util.SystemUtil.eventually +import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow +import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity +import com.android.compatibility.common.util.ThrowingSupplier +import com.android.compatibility.common.util.UiAutomatorUtils +import com.android.compatibility.common.util.UiDumpUtils +import com.android.compatibility.common.util.click +import com.android.compatibility.common.util.depthFirstSearch +import com.android.compatibility.common.util.textAsString +import java.io.InputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.junit.Assert +import org.junit.Assert.assertThat +import org.junit.Assert.assertTrue + +private const val BROADCAST_TIMEOUT_MS = 60000L + +const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled" +const val HIBERNATION_BOOT_RECEIVER_CLASS_NAME = + "com.android.permissioncontroller.hibernation.HibernationOnBootReceiver" +const val ACTION_SET_UP_HIBERNATION = + "com.android.permissioncontroller.action.SET_UP_HIBERNATION" + +const val SYSUI_PKG_NAME = "com.android.systemui" +const val NOTIF_LIST_ID = "com.android.systemui:id/notification_stack_scroller" +const val CLEAR_ALL_BUTTON_ID = "dismiss_text" +// Time to find a notification. Unlikely, but in cases with a lot of notifications, it may take +// time to find the notification we're looking for +const val NOTIF_FIND_TIMEOUT = 20000L +const val VIEW_WAIT_TIMEOUT = 3000L + +const val CMD_EXPAND_NOTIFICATIONS = "cmd statusbar expand-notifications" +const val CMD_COLLAPSE = "cmd statusbar collapse" + +const val APK_PATH_S_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeSApp.apk" +const val APK_PACKAGE_NAME_S_APP = "android.hibernation.cts.autorevokesapp" +const val APK_PATH_R_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeRApp.apk" +const val APK_PACKAGE_NAME_R_APP = "android.hibernation.cts.autorevokerapp" +const val APK_PATH_Q_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeQApp.apk" +const val APK_PACKAGE_NAME_Q_APP = "android.hibernation.cts.autorevokeqapp" + +fun runBootCompleteReceiver(context: Context, testTag: String) { + val pkgManager = context.packageManager + val permissionControllerPkg = pkgManager.permissionControllerPackageName + var permissionControllerSetupIntent = Intent(ACTION_SET_UP_HIBERNATION).apply { + setPackage(permissionControllerPkg) + setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + val receivers = pkgManager.queryBroadcastReceivers( + permissionControllerSetupIntent, /* flags= */ 0) + if (receivers.size == 0) { + // May be on an older, pre-built PermissionController. In this case, try sending directly. + permissionControllerSetupIntent = Intent().apply { + setPackage(permissionControllerPkg) + setClassName(permissionControllerPkg, HIBERNATION_BOOT_RECEIVER_CLASS_NAME) + setFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + } + val countdownLatch = CountDownLatch(1) + Log.d(testTag, "Sending boot complete broadcast directly to $permissionControllerPkg") + context.sendOrderedBroadcast( + permissionControllerSetupIntent, + /* receiverPermission= */ null, + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + countdownLatch.countDown() + Log.d(testTag, "Broadcast received by $permissionControllerPkg") + } + }, + Handler.createAsync(Looper.getMainLooper()), + Activity.RESULT_OK, + /* initialData= */ null, + /* initialExtras= */ null) + assertTrue("Timed out while waiting for boot receiver broadcast to be received", + countdownLatch.await(BROADCAST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) +} + +fun runAppHibernationJob(context: Context, tag: String) { + val logcat = Logcat() + + // Sometimes first run observes stale package data + // so run twice to prevent that + repeat(2) { + val mark = logcat.mark(tag) + eventually { + runShellCommandOrThrow("cmd jobscheduler run -u " + + "${Process.myUserHandle().identifier} -f " + + "${context.packageManager.permissionControllerPackageName} 2") + } + logcat.assertLogcatContainsInOrder("*:*", 30_000, + mark, + "onStartJob", + "Done auto-revoke for user") + } +} + +fun runPermissionEventCleanupJob(context: Context) { + eventually { + runShellCommandOrThrow("cmd jobscheduler run -u " + + "${Process.myUserHandle().identifier} -f " + + "${context.packageManager.permissionControllerPackageName} 3") + } +} + +inline fun withApp( + apk: String, + packageName: String, + action: () -> Unit +) { + installApk(apk) + try { + // Try to reduce flakiness caused by new package update not propagating in time + Thread.sleep(1000) + action() + } finally { + uninstallApp(packageName) + } +} + +inline fun withAppNoUninstallAssertion( + apk: String, + packageName: String, + action: () -> Unit +) { + installApk(apk) + try { + // Try to reduce flakiness caused by new package update not propagating in time + Thread.sleep(1000) + action() + } finally { + uninstallAppWithoutAssertion(packageName) + } +} + +inline fun <T> withDeviceConfig( + namespace: String, + name: String, + value: String, + action: () -> T +): T { + val oldValue = runWithShellPermissionIdentity(ThrowingSupplier { + DeviceConfig.getProperty(namespace, name) + }) + try { + runWithShellPermissionIdentity { + DeviceConfig.setProperty(namespace, name, value, false /* makeDefault */) + } + return action() + } finally { + runWithShellPermissionIdentity { + DeviceConfig.setProperty(namespace, name, oldValue, false /* makeDefault */) + } + } +} + +inline fun <T> withUnusedThresholdMs(threshold: Long, action: () -> T): T { + return withDeviceConfig( + DeviceConfig.NAMESPACE_PERMISSIONS, "auto_revoke_unused_threshold_millis2", + threshold.toString(), action) +} + +inline fun <T> withSafetyCenterEnabled(action: () -> T): T { + return withDeviceConfig( + DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_SAFETY_CENTER_ENABLED, + true.toString(), action) +} + +fun awaitAppState(pkg: String, stateMatcher: Matcher<Int>) { + val context: Context = InstrumentationRegistry.getTargetContext() + eventually { + runWithShellPermissionIdentity { + val packageImportance = context + .getSystemService(ActivityManager::class.java)!! + .getPackageImportance(pkg) + assertThat(packageImportance, stateMatcher) + } + } +} + +fun startApp(packageName: String) { + val context = InstrumentationRegistry.getTargetContext() + val intent = context.packageManager.getLaunchIntentForPackage(packageName) + context.startActivity(intent) + awaitAppState(packageName, Matchers.lessThanOrEqualTo(IMPORTANCE_TOP_SLEEPING)) + waitForIdle() +} + +fun goHome() { + runShellCommandOrThrow("input keyevent KEYCODE_HOME") + waitForIdle() +} + +/** + * Open the "unused apps" notification which is sent after the hibernation job. + */ +fun openUnusedAppsNotification() { + val notifSelector = By.textContains("unused app") + if (hasFeatureWatch()) { + val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation + expandNotificationsWatch(UiAutomatorUtils.getUiDevice()) + waitFindObject(uiAutomation, notifSelector).click() + // In wear os, notification has one additional button to open it + waitFindObject(uiAutomation, By.text("Open")).click() + } else { + runShellCommandOrThrow(CMD_EXPAND_NOTIFICATIONS) + waitFindNotification(notifSelector, NOTIF_FIND_TIMEOUT).click() + } +} + +fun hasFeatureWatch(): Boolean { + return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature( + PackageManager.FEATURE_WATCH) +} + +fun hasFeatureTV(): Boolean { + return InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature( + PackageManager.FEATURE_LEANBACK) || + InstrumentationRegistry.getTargetContext().packageManager.hasSystemFeature( + PackageManager.FEATURE_TELEVISION) +} + +private fun expandNotificationsWatch(uiDevice: UiDevice) { + with(uiDevice) { + wakeUp() + // Swipe up from bottom to reveal notifications + val x = displayWidth / 2 + swipe(x, displayHeight, x, 0, 1) + } +} + +/** + * Reset to the top of the notifications list. + */ +private fun resetNotifications(notificationList: UiScrollable) { + runShellCommandOrThrow(CMD_COLLAPSE) + notificationList.waitUntilGone(VIEW_WAIT_TIMEOUT) + runShellCommandOrThrow(CMD_EXPAND_NOTIFICATIONS) +} + +private fun waitFindNotification(selector: BySelector, timeoutMs: Long): + UiObject2 { + var view: UiObject2? = null + val start = System.currentTimeMillis() + val uiDevice = UiAutomatorUtils.getUiDevice() + + var isAtEnd = false + var wasScrolledUpAlready = false + while (view == null && start + timeoutMs > System.currentTimeMillis()) { + view = uiDevice.wait(Until.findObject(selector), VIEW_WAIT_TIMEOUT) + if (view == null) { + val notificationList = UiScrollable(UiSelector().resourceId(NOTIF_LIST_ID)) + wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) { + Assert.assertTrue("Notification list view not found", + notificationList.waitForExists(VIEW_WAIT_TIMEOUT)) + } + if (isAtEnd) { + if (wasScrolledUpAlready) { + break + } + resetNotifications(notificationList) + isAtEnd = false + wasScrolledUpAlready = true + } else { + notificationList.scrollForward() + isAtEnd = uiDevice.hasObject(By.res(SYSUI_PKG_NAME, CLEAR_ALL_BUTTON_ID)) + } + } + } + wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) { + Assert.assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector, + view) + } + return view!! +} + +fun waitFindObject(uiAutomation: UiAutomation, selector: BySelector): UiObject2 { + try { + return UiAutomatorUtils.waitFindObject(selector) + } catch (e: RuntimeException) { + val ui = uiAutomation.rootInActiveWindow + + val title = ui.depthFirstSearch { node -> + node.viewIdResourceName?.contains("alertTitle") == true + } + val okButton = ui.depthFirstSearch { node -> + node.textAsString?.equals("OK", ignoreCase = true) ?: false + } + + if (title?.text?.toString() == "Android System" && okButton != null) { + // Auto dismiss occasional system dialogs to prevent interfering with the test + android.util.Log.w(AutoRevokeTest.LOG_TAG, "Ignoring exception", e) + okButton.click() + return UiAutomatorUtils.waitFindObject(selector) + } else { + throw e + } + } +} + +class Logcat() : LogcatInspector() { + override fun executeShellCommand(command: String?): InputStream { + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + return ParcelFileDescriptor.AutoCloseInputStream( + instrumentation.uiAutomation.executeShellCommand(command)) + } +} |