Rewrite "waitForObject" for notification UI
Tests consistently fail due to standard scrolling assumptions not being
applicable to the notification UI. We address this by writing our own
version of waitForObject, waitForNotification, that deals with these
issues. Namely
- Instead of ANY scrollable, it only scrolls the notification list (as
identified by the resource id)
- The end of the list is determined by when the "Clear all" button
appears (the standard way does not work as it continues to scroll until
the notification panel is closed)
- Resetting to the top is done by closing and opening the notification
list again
We also move a lot of common utility to AppHibernationUtils to be used
by both AutoRevokeTest and AppHibernationIntegrationTest
Bug: 191506099
Test: atest AutoRevokeTest
Test: atest AppHibernationIntegrationTest
Test: forrest runs
Change-Id: I99bb2eca81f40835ef279ddd714574c24ed8776f
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt b/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
index a7e893d..e2718df 100644
--- a/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
+++ b/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
@@ -21,8 +21,8 @@
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
-import android.os.Build
import android.net.Uri
+import android.os.Build
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
import android.provider.Settings
@@ -114,9 +114,8 @@
packageManager.getApplicationInfo(APK_PACKAGE_NAME_S_APP, 0 /* flags */)
val stopped = ((ai.flags and ApplicationInfo.FLAG_STOPPED) != 0)
assertTrue(stopped)
- runShellCommandOrThrow("cmd statusbar expand-notifications")
- waitFindObject(By.textContains("unused app"))
- .click()
+ openUnusedAppsNotification()
+
waitFindObject(By.text(APK_PACKAGE_NAME_S_APP))
}
}
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
index faae778..da55409 100644
--- a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
+++ b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
@@ -21,26 +21,46 @@
import android.app.Instrumentation
import android.app.UiAutomation
import android.content.Context
+import android.content.pm.PackageManager
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 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 org.hamcrest.Matcher
import org.hamcrest.Matchers
+import org.junit.Assert
import org.junit.Assert.assertThat
import java.io.InputStream
+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 = 1000L
+
+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/os/CtsAutoRevokeSApp.apk"
const val APK_PACKAGE_NAME_S_APP = "android.os.cts.autorevokesapp"
const val APK_PATH_R_APP = "/data/local/tmp/cts/os/CtsAutoRevokeRApp.apk"
@@ -148,6 +168,82 @@
runShellCommandOrThrow("input keyevent KEYCODE_HOME")
}
+/**
+ * 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)
+}
+
+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)
diff --git a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
index e17883c..d5cc564 100644
--- a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
+++ b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
@@ -31,7 +31,6 @@
import android.platform.test.annotations.AppModeFull
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.UiObjectNotFoundException
import android.view.accessibility.AccessibilityNodeInfo
@@ -84,7 +83,6 @@
private val context: Context = InstrumentationRegistry.getTargetContext()
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
- private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
private val mPermissionControllerResources: Resources = context.createPackageContext(
context.packageManager.permissionControllerPackageName, 0).resources
@@ -392,19 +390,6 @@
assertPermission(PERMISSION_GRANTED)
}
- private fun openUnusedAppsNotification() {
- if (hasFeatureWatch()) {
- expandNotificationsWatch()
- } else {
- runShellCommandOrThrow("cmd statusbar expand-notifications")
- }
- waitFindObject(By.textContains("unused app")).click()
- if (hasFeatureWatch()) {
- // In wear os, notification has one additional button to open it
- waitFindObject(By.text("Open")).click()
- }
- }
-
private fun goBack() {
runShellCommandOrThrow("input keyevent KEYCODE_BACK")
}
@@ -500,19 +485,6 @@
waitForIdle()
}
- private fun hasFeatureWatch(): Boolean {
- return context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
- }
-
- private fun expandNotificationsWatch() {
- with(uiDevice) {
- wakeUp()
- // Swipe up from bottom to reveal notifications
- val x = displayWidth / 2
- swipe(x, displayHeight, x, 0, 1)
- }
- }
-
private fun assertAllowlistState(state: Boolean) {
assertThat(
waitFindObject(By.textStartsWith("Auto-revoke allowlisted: ")).text,