Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f60eda5b authored by Siarhei Vishniakou's avatar Siarhei Vishniakou
Browse files

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
parent 1da632ed
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ android_test {
        "modules-utils-testable-device-config-defaults",
    ],
    srcs: [
        "src/**/*.aidl",
        "src/**/*.java",
        "src/**/*.kt",
    ],
+3 −3
Original line number Diff line number Diff line
@@ -76,7 +76,7 @@ import org.mockito.Mockito.spy
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 @@ class InputManagerServiceTests {

    @Test
    fun testStart() {
        verifyZeroInteractions(native)
        verifyNoInteractions(native)

        service.start()
        verify(native).start()
@@ -217,7 +217,7 @@ class InputManagerServiceTests {

    @Test
    fun testInputSettingsUpdatedOnSystemRunning() {
        verifyZeroInteractions(native)
        verifyNoInteractions(native)

        runWithShellPermissionIdentity {
            service.systemRunning()
+93 −59
Original line number Diff line number Diff line
@@ -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
@@ -50,14 +55,35 @@ import org.junit.Rule
import org.junit.Test
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 @@ import org.junit.runner.RunWith
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
            }

            override fun notifyMotion(event: MotionEvent) {
                remoteInputEvents.add(event)
            }
        }

    @get:Rule val showErrorDialogs = ShowErrorDialogsRule()

    @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)
@@ -120,10 +159,10 @@ class AnrTest {
        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)
    }
@@ -144,7 +183,7 @@ class AnrTest {
        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 @@ class AnrTest {
        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 }))
    }
}
+17 −0
Original line number Diff line number Diff line
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);
}
+21 −4
Original line number Diff line number Diff line
@@ -23,20 +23,24 @@ import android.app.Activity
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) :
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 @@ class UnresponsiveGestureMonitorActivity : Activity() {
    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(),
        )
    }
}