Use startActivity to launch ANR app
This will allow us to communicate with the app, so that we can learn
about the app's window token, displayId, and PID. This will also allow
the app to notify the test when the input event is received. This will
help the test avoid closing the uinput device prematurely.
Outstanding issues in this test:
1. Some of the injection that's occurring is still done using
"uiobject::click" instead of using uinput. The difficulty here is
that we need a way to wait for the ANR window to disappear after
clicking to prevent the uinput device from getting closed too soon.
To do this, we may be able to match a window by the window's name.
This kind of refactor should be done in a separate CL.
2. The test needs to be skipped on devices that don't support error
dialogs.
Bug: 339924248
Flag: TEST_ONLY
Test: atest AnrTest
Change-Id: Ic944d8a2558237d29a6155abe2a0271cb3e982b6
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
index 1f0bd61..544f94b 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -15,6 +15,7 @@
"modules-utils-testable-device-config-defaults",
],
srcs: [
+ "src/**/*.aidl",
"src/**/*.java",
"src/**/*.kt",
],
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index eac4267..7ec8f9c 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -76,7 +76,7 @@
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.verifyNoInteractions
import org.mockito.Mockito.`when`
import org.mockito.stubbing.OngoingStubbing
@@ -209,7 +209,7 @@
@Test
fun testStart() {
- verifyZeroInteractions(native)
+ verifyNoInteractions(native)
service.start()
verify(native).start()
@@ -217,7 +217,7 @@
@Test
fun testInputSettingsUpdatedOnSystemRunning() {
- verifyZeroInteractions(native)
+ verifyNoInteractions(native)
runWithShellPermissionIdentity {
service.systemRunning()
diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt
index 73192ea..f8cb86b 100644
--- a/tests/Input/src/com/android/test/input/AnrTest.kt
+++ b/tests/Input/src/com/android/test/input/AnrTest.kt
@@ -15,32 +15,37 @@
*/
package com.android.test.input
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.filters.MediumTest
-
import android.app.ActivityManager
import android.app.ApplicationExitInfo
-import android.content.Context
-import android.graphics.Rect
+import android.app.Instrumentation
+import android.content.Intent
import android.hardware.display.DisplayManager
import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
import android.os.IInputConstants.UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS
import android.os.SystemClock
-import android.server.wm.CtsWindowInfoUtils.waitForStableWindowGeometry
+import android.server.wm.CtsWindowInfoUtils.getWindowCenter
+import android.server.wm.CtsWindowInfoUtils.waitForWindowOnTop
import android.testing.PollingCheck
-
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_DOWN
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
-
+import com.android.cts.input.BlockingQueueEventVerifier
import com.android.cts.input.DebugInputRule
import com.android.cts.input.ShowErrorDialogsRule
import com.android.cts.input.UinputTouchScreen
-
+import com.android.cts.input.inputeventmatchers.withMotionAction
import java.time.Duration
-
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.function.Supplier
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -51,13 +56,34 @@
import org.junit.runner.RunWith
/**
+ * Click on the center of the window identified by the provided window token.
+ * The click is performed using "UinputTouchScreen" device.
+ * If the touchscreen device is closed too soon, it may cause the click to be dropped. Therefore,
+ * the provided runnable can ensure that the click is delivered before the device is closed, thus
+ * avoiding this race.
+ */
+private fun clickOnWindow(
+ token: IBinder,
+ displayId: Int,
+ instrumentation: Instrumentation,
+ waitForEvent: Runnable,
+) {
+ val displayManager = instrumentation.context.getSystemService(DisplayManager::class.java)
+ val display = displayManager.getDisplay(displayId)
+ val point = getWindowCenter({ token }, display.displayId)
+ UinputTouchScreen(instrumentation, display).use { touchScreen ->
+ touchScreen.touchDown(point.x, point.y).lift()
+ // If the device is allowed to close without waiting here, the injected click may be dropped
+ waitForEvent.run()
+ }
+}
+
+/**
* This test makes sure that an unresponsive gesture monitor gets an ANR.
*
* The gesture monitor must be registered from a different process than the instrumented process.
- * Otherwise, when the test runs, you will get:
- * Test failed to run to completion.
- * Reason: 'Instrumentation run failed due to 'keyDispatchingTimedOut''.
- * Check device logcat for details
+ * Otherwise, when the test runs, you will get: Test failed to run to completion. Reason:
+ * 'Instrumentation run failed due to 'keyDispatchingTimedOut''. Check device logcat for details
* RUNNER ERROR: Instrumentation run failed due to 'keyDispatchingTimedOut'
*/
@MediumTest
@@ -65,30 +91,43 @@
class AnrTest {
companion object {
private const val TAG = "AnrTest"
- private const val ALL_PIDS = 0
private const val NO_MAX = 0
}
private val instrumentation = InstrumentationRegistry.getInstrumentation()
- private var hideErrorDialogs = 0
private lateinit var PACKAGE_NAME: String
- private val DISPATCHING_TIMEOUT = (UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS *
- Build.HW_TIMEOUT_MULTIPLIER)
+ private val DISPATCHING_TIMEOUT =
+ (UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS * Build.HW_TIMEOUT_MULTIPLIER)
+ private var remoteWindowToken: IBinder? = null
+ private var remoteDisplayId: Int? = null
+ private var remotePid: Int? = null
+ private val remoteInputEvents = LinkedBlockingQueue<InputEvent>()
+ private val verifier = BlockingQueueEventVerifier(remoteInputEvents)
- @get:Rule
- val debugInputRule = DebugInputRule()
+ val binder =
+ object : IAnrTestService.Stub() {
+ override fun provideActivityInfo(token: IBinder, displayId: Int, pid: Int) {
+ remoteWindowToken = token
+ remoteDisplayId = displayId
+ remotePid = pid
+ }
- @get:Rule
- val showErrorDialogs = ShowErrorDialogsRule()
+ override fun notifyMotion(event: MotionEvent) {
+ remoteInputEvents.add(event)
+ }
+ }
+
+ @get:Rule val showErrorDialogs = ShowErrorDialogsRule()
+
+ @get:Rule val debugInputRule = DebugInputRule()
@Before
fun setUp() {
+ startUnresponsiveActivity()
PACKAGE_NAME = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.getName()
}
- @After
- fun tearDown() {
- }
+ @After fun tearDown() {}
@Test
@DebugInputRule.DebugInput(bug = 339924248)
@@ -112,7 +151,7 @@
val timestamp = System.currentTimeMillis()
val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
val closeAppButton: UiObject2? =
- uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000)
+ uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000)
if (closeAppButton == null) {
fail("Could not find anr dialog/close button")
return
@@ -120,10 +159,10 @@
closeAppButton.click()
/**
* We must wait for the app to be fully closed before exiting this test. This is because
- * another test may again invoke 'am start' for the same activity.
- * If the 1st process that got ANRd isn't killed by the time second 'am start' runs,
- * the killing logic will apply to the newly launched 'am start' instance, and the second
- * test will fail because the unresponsive activity will never be launched.
+ * another test may again invoke 'am start' for the same activity. If the 1st process that
+ * got ANRd isn't killed by the time second 'am start' runs, the killing logic will apply to
+ * the newly launched 'am start' instance, and the second test will fail because the
+ * unresponsive activity will never be launched.
*/
waitForNewExitReasonAfter(timestamp)
}
@@ -132,7 +171,7 @@
// Find anr dialog and tap on wait
val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
val waitButton: UiObject2? =
- uiDevice.wait(Until.findObject(By.res("android:id/aerr_wait")), 20000)
+ uiDevice.wait(Until.findObject(By.res("android:id/aerr_wait")), 20000)
if (waitButton == null) {
fail("Could not find anr dialog/wait button")
return
@@ -144,7 +183,7 @@
lateinit var infos: List<ApplicationExitInfo>
instrumentation.runOnMainSync {
val am = instrumentation.getContext().getSystemService(ActivityManager::class.java)!!
- infos = am.getHistoricalProcessExitReasons(PACKAGE_NAME, ALL_PIDS, NO_MAX)
+ infos = am.getHistoricalProcessExitReasons(PACKAGE_NAME, remotePid!!, NO_MAX)
}
return infos
}
@@ -159,37 +198,32 @@
assertEquals(ApplicationExitInfo.REASON_ANR, reasons[0].reason)
}
- private fun clickOnObject(obj: UiObject2) {
- val displayManager =
- instrumentation.context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
- val display = displayManager.getDisplay(obj.getDisplayId())
- val rect: Rect = obj.visibleBounds
- UinputTouchScreen(instrumentation, display).use { touchScreen ->
- touchScreen
- .touchDown(rect.centerX(), rect.centerY())
- .lift()
- }
- }
-
private fun triggerAnr() {
- startUnresponsiveActivity()
- val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
- val obj: UiObject2? = uiDevice.wait(Until.findObject(By.pkg(PACKAGE_NAME)), 10000)
-
- if (obj == null) {
- fail("Could not find unresponsive activity")
- return
- }
-
- clickOnObject(obj)
+ clickOnWindow(
+ remoteWindowToken!!,
+ remoteDisplayId!!,
+ instrumentation,
+ ) { verifier.assertReceivedMotion(withMotionAction(ACTION_DOWN)) }
SystemClock.sleep(DISPATCHING_TIMEOUT.toLong()) // default ANR timeout for gesture monitors
}
private fun startUnresponsiveActivity() {
- val flags = " -W -n "
- val startCmd = "am start $flags $PACKAGE_NAME/.UnresponsiveGestureMonitorActivity"
- instrumentation.uiAutomation.executeShellCommand(startCmd)
- waitForStableWindowGeometry(Duration.ofSeconds(5))
+ val intent =
+ Intent(instrumentation.targetContext, UnresponsiveGestureMonitorActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+ val bundle = Bundle()
+ bundle.putBinder("serviceBinder", binder)
+ intent.putExtra("serviceBundle", bundle)
+ instrumentation.targetContext.startActivity(intent)
+ // first, wait for the token to become valid
+ PollingCheck.check(
+ "UnresponsiveGestureMonitorActivity failed to call 'provideActivityInfo'",
+ Duration.ofSeconds(5).toMillis()) { remoteWindowToken != null }
+ // next, wait for the window of the activity to get on top
+ // we could combine the two checks above, but the current setup makes it easier to detect
+ // errors
+ assertTrue("Remote activity window did not become visible",
+ waitForWindowOnTop(Duration.ofSeconds(5), Supplier { remoteWindowToken }))
}
}
diff --git a/tests/Input/src/com/android/test/input/IAnrTestService.aidl b/tests/Input/src/com/android/test/input/IAnrTestService.aidl
new file mode 100644
index 0000000..e3caf06
--- /dev/null
+++ b/tests/Input/src/com/android/test/input/IAnrTestService.aidl
@@ -0,0 +1,17 @@
+package com.android.test.input;
+
+import android.view.MotionEvent;
+
+interface IAnrTestService {
+ /**
+ * Provide the activity information. This includes:
+ * windowToken: the windowToken of the activity window
+ * displayId: the display id on which the activity is positioned
+ * pid: the pid of the activity
+ */
+ void provideActivityInfo(IBinder windowToken, int displayId, int pid);
+ /**
+ * Provide the MotionEvent received by the remote activity.
+ */
+ void notifyMotion(in MotionEvent event);
+}
diff --git a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt
index 1842f0a..1e44617 100644
--- a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt
+++ b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt
@@ -23,20 +23,24 @@
import android.hardware.input.InputManager
import android.os.Bundle
import android.os.Looper
+import android.os.Process
import android.util.Log
import android.view.InputChannel
import android.view.InputEvent
import android.view.InputEventReceiver
import android.view.InputMonitor
+import android.view.MotionEvent
-class UnresponsiveReceiver(channel: InputChannel, looper: Looper) :
- InputEventReceiver(channel, looper) {
+class UnresponsiveReceiver(channel: InputChannel, looper: Looper, val service: IAnrTestService) :
+ InputEventReceiver(channel, looper) {
companion object {
const val TAG = "UnresponsiveReceiver"
}
+
override fun onInputEvent(event: InputEvent) {
Log.i(TAG, "Received $event")
// Not calling 'finishInputEvent' in order to trigger the ANR
+ service.notifyMotion(event as MotionEvent)
}
}
@@ -44,14 +48,27 @@
companion object {
const val MONITOR_NAME = "unresponsive gesture monitor"
}
+
private lateinit var mInputEventReceiver: InputEventReceiver
private lateinit var mInputMonitor: InputMonitor
+ private lateinit var service: IAnrTestService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val bundle = intent.getBundleExtra("serviceBundle")!!
+ service = IAnrTestService.Stub.asInterface(bundle.getBinder("serviceBinder"))
val inputManager = checkNotNull(getSystemService(InputManager::class.java))
mInputMonitor = inputManager.monitorGestureInput(MONITOR_NAME, displayId)
- mInputEventReceiver = UnresponsiveReceiver(
- mInputMonitor.getInputChannel(), Looper.myLooper()!!)
+ mInputEventReceiver =
+ UnresponsiveReceiver(mInputMonitor.getInputChannel(), Looper.myLooper()!!, service)
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ service.provideActivityInfo(
+ window.decorView.windowToken,
+ display.displayId,
+ Process.myPid(),
+ )
}
}