Add tests for input injection from various sources

Add tests that can be used to verify that input injection succeeds for
tests from the supported sources, which include Instrumentation and
UiAutomation.

These tests verify injection into a sparate test apk that has a
different uid than the tests. This also allows us to test that apps
that don't have the INJECT_EVENTS permission cannot inject events.

Bug: 229830384
Test: atest InputInjectionTest
Change-Id: Id60b552b5b6b10c83be1ef529e39f97c8a861e93
diff --git a/tests/input/Android.bp b/tests/input/Android.bp
index 05178e0..45b4758 100644
--- a/tests/input/Android.bp
+++ b/tests/input/Android.bp
@@ -25,13 +25,19 @@
         "general-tests",
     ],
     compile_multilib: "both",
-    srcs: ["src/**/*.kt"],
+
+    srcs: [
+        "src/**/*.kt",
+        "InputInjectionApp/src/com/android/test/inputinjection/IInputInjectionTestCallbacks.aidl",
+    ],
     resource_dirs: ["res"],
     static_libs: [
         "androidx.test.core",
         "androidx.test.ext.junit",
         "compatibility-device-util-axt",
         "cts-input-lib",
+        "cts-wm-util",
+        "kotlin-test",
     ],
     sdk_version: "test_current",
 }
diff --git a/tests/input/AndroidTest.xml b/tests/input/AndroidTest.xml
index 78eeff5..63cb14a 100644
--- a/tests/input/AndroidTest.xml
+++ b/tests/input/AndroidTest.xml
@@ -22,6 +22,7 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsInputTestCases.apk" />
+        <option name="test-file-name" value="InputInjectionApp.apk" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
diff --git a/tests/input/InputInjectionApp/Android.bp b/tests/input/InputInjectionApp/Android.bp
new file mode 100644
index 0000000..c83cf74
--- /dev/null
+++ b/tests/input/InputInjectionApp/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2022 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "InputInjectionApp",
+    defaults: ["cts_support_defaults"],
+    srcs: [
+        "**/*.kt",
+        "**/*.aidl",
+    ],
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+    sdk_version: "current",
+}
diff --git a/tests/input/InputInjectionApp/AndroidManifest.xml b/tests/input/InputInjectionApp/AndroidManifest.xml
new file mode 100644
index 0000000..73ce232
--- /dev/null
+++ b/tests/input/InputInjectionApp/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.inputinjection">
+    <application android:label="Input Injection">
+        <activity android:name="InputInjectionActivity"
+                  android:launchMode="singleInstance"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.test.inputinjection.action.TEST_INJECTION"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/input/InputInjectionApp/OWNERS b/tests/input/InputInjectionApp/OWNERS
new file mode 100644
index 0000000..c88bfe9
--- /dev/null
+++ b/tests/input/InputInjectionApp/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/INPUT_OWNERS
diff --git a/tests/input/InputInjectionApp/res/layout/activity_input_injection.xml b/tests/input/InputInjectionApp/res/layout/activity_input_injection.xml
new file mode 100644
index 0000000..f864566
--- /dev/null
+++ b/tests/input/InputInjectionApp/res/layout/activity_input_injection.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:text="Input Injection Activity"/>
+
+</LinearLayout>
diff --git a/tests/input/InputInjectionApp/src/com/android/test/inputinjection/IInputInjectionTestCallbacks.aidl b/tests/input/InputInjectionApp/src/com/android/test/inputinjection/IInputInjectionTestCallbacks.aidl
new file mode 100644
index 0000000..a422ebf
--- /dev/null
+++ b/tests/input/InputInjectionApp/src/com/android/test/inputinjection/IInputInjectionTestCallbacks.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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 com.android.test.inputinjection;
+
+interface IInputInjectionTestCallbacks {
+
+    void onKeyEvent(in android.view.KeyEvent event);
+
+    void onTouchEvent(in android.view.MotionEvent event);
+
+    void onTestInjectionFromApp(in List<String> errors);
+}
diff --git a/tests/input/InputInjectionApp/src/com/android/test/inputinjection/InputInjectionActivity.kt b/tests/input/InputInjectionApp/src/com/android/test/inputinjection/InputInjectionActivity.kt
new file mode 100644
index 0000000..200d4f9
--- /dev/null
+++ b/tests/input/InputInjectionApp/src/com/android/test/inputinjection/InputInjectionActivity.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 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 com.android.test.inputinjection
+
+import android.app.Activity
+import android.app.Instrumentation
+import android.content.Intent
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import java.util.concurrent.Executors
+
+/**
+ * This app is used for testing the input injection. It's used in 2 ways:
+ * 1) This app tries to use Instrumentation APIs to inject various events. All of these injection
+ *    attempts should fail because it does not have INJECT_EVENTS permission. The results of the
+ *    injection are reported by this app via the IInputInjectionTestCallbacks interface.
+ * 2) The test code tries to inject events into this app. Any keys or motions received by
+ *    this app are reported back to the test via the IInputInjectionTestCallbacks interface.
+ */
+class InputInjectionActivity : Activity() {
+
+    companion object {
+        const val INTENT_ACTION_TEST_INJECTION =
+            "com.android.test.inputinjection.action.TEST_INJECTION"
+        const val INTENT_EXTRA_CALLBACK = "com.android.test.inputinjection.extra.CALLBACK"
+
+        // We don't need to send matching "UP" events for the injected keys and motions because
+        // under normal circumstances, these injections would fail so the gesture would never
+        // actually start.
+        val injectionMethods = listOf<Pair<String, (View) -> Unit>>(
+            Pair("sendPointerSync", { view ->
+                Instrumentation().sendPointerSync(getMotionDownInView(view))
+            }),
+            Pair("sendTrackballEventSync", { view ->
+                Instrumentation().sendTrackballEventSync(getMotionDownInView(view))
+            }),
+            Pair("sendKeySync", {
+                Instrumentation().sendKeySync(getKeyDown())
+            }),
+            Pair("sendKeyUpDownSync", {
+                Instrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_A)
+            }),
+            Pair("sendCharacterSync", {
+                Instrumentation().sendCharacterSync(KeyEvent.KEYCODE_A)
+            }),
+            Pair("sendStringSync", {
+                Instrumentation().sendStringSync("Hello World!")
+            })
+        )
+    }
+
+    // The binder callbacks that report results back to the test process
+    private lateinit var callbacks: IInputInjectionTestCallbacks
+
+    private lateinit var view: View
+
+    public override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_input_injection)
+
+        callbacks = IInputInjectionTestCallbacks.Stub.asInterface(
+            intent.extras?.getBinder(INTENT_EXTRA_CALLBACK)
+        ) ?: throw IllegalStateException("InputInjectionActivity started without binder callback")
+
+        view = findViewById(R.id.view)!!
+    }
+
+    override fun onNewIntent(intent: Intent?) {
+        if (intent!!.action == INTENT_ACTION_TEST_INJECTION) {
+            Executors.newSingleThreadExecutor().execute(this::testInputInjectionFromApp)
+        }
+    }
+
+    /**
+     * Attempt to inject input events from this application into the system, and report the result
+     * to the test process through the [callbacks]. Since this method synchronously injects events,
+     * it must not be called from the main thread.
+     */
+    private fun testInputInjectionFromApp() {
+        val errors = mutableListOf<String>()
+        for ((name, inject) in injectionMethods) {
+            try {
+                inject(view)
+                errors.add(
+                    "Call to $name succeeded without throwing an exception " +
+                            "from an app that does not have INJECT_EVENTS permission."
+                )
+            } catch (e: RuntimeException) {
+                // We expect a security exception to be thrown because this app does not have
+                // the INJECT_EVENTS permission.
+            }
+        }
+        callbacks.onTestInjectionFromApp(errors)
+    }
+
+    override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+        callbacks.onKeyEvent(event)
+        return super.dispatchKeyEvent(event)
+    }
+
+    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
+        callbacks.onTouchEvent(event)
+        return super.dispatchTouchEvent(event)
+    }
+}
+
+private fun getMotionDownInView(view: View): MotionEvent {
+    val now = SystemClock.uptimeMillis()
+    val (x, y) = view.getCenterOnScreen()
+    // Use the default source and allow the injection methods to configure it if needed.
+    return MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, x, y, 0)
+}
+
+private fun getKeyDown(): KeyEvent {
+    val now = SystemClock.uptimeMillis()
+    return KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0 /*repeat*/)
+}
+
+private fun View.getCenterOnScreen(): Pair<Float, Float> {
+    val location = IntArray(2).also { getLocationOnScreen(it) }
+    return location[0].toFloat() + width / 2f to location[1].toFloat() + height / 2f
+}
diff --git a/tests/input/src/android/input/cts/InputInjectionTest.kt b/tests/input/src/android/input/cts/InputInjectionTest.kt
new file mode 100644
index 0000000..8298006
--- /dev/null
+++ b/tests/input/src/android/input/cts/InputInjectionTest.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2022 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.input.cts
+
+import android.app.ActivityManager
+import android.app.Instrumentation
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Bundle
+import android.os.SystemClock
+import android.server.wm.WindowManagerState
+import android.view.InputEvent
+import android.view.KeyEvent
+import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
+import com.android.test.inputinjection.IInputInjectionTestCallbacks
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+/**
+ * Test case for injecting input through various means.
+ *
+ * We verify that injection succeeds only in the following two cases:
+ * 1. The calling uid must have the [android.Manifest.permission.INJECT_EVENTS]; OR
+ * 2. The caller must be instrumented by an uid that has the same permission (e.g. tests that
+ *    don't have the permission that are instrumented by the Shell).
+ */
+@RunWith(AndroidJUnit4::class)
+class InputInjectionTest {
+
+    companion object {
+        const val WAIT_FOR_FOCUS_TIMEOUT_MILLS = 5000L
+
+        const val INPUT_INJECTION_PACKAGE = "com.android.test.inputinjection"
+        const val INPUT_INJECTION_ACTIVITY =
+            "$INPUT_INJECTION_PACKAGE.InputInjectionActivity"
+        val INPUT_INJECTION_COMPONENT =
+            ComponentName(INPUT_INJECTION_PACKAGE, INPUT_INJECTION_ACTIVITY)
+        const val INTENT_ACTION_TEST_INJECTION =
+            "com.android.test.inputinjection.action.TEST_INJECTION"
+        const val INTENT_EXTRA_CALLBACK = "com.android.test.inputinjection.extra.CALLBACK"
+
+        val expectNoEvent =
+            { e: InputEvent -> fail("Expected no input events, but got: $e") }
+        val expectNoInjectionResult =
+            { _: Any -> fail("No injection result expected") }
+    }
+
+    private lateinit var instrumentation: Instrumentation
+    private lateinit var targetContext: Context
+    private lateinit var activityManager: ActivityManager
+
+    @Before
+    fun setUp() {
+        instrumentation = InstrumentationRegistry.getInstrumentation()!!
+        targetContext = instrumentation.targetContext
+        activityManager = instrumentation.context
+            .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+    }
+
+    /**
+     * Verify that any non-instrumented app that does not have
+     * [android.Manifest.permission.INJECT_EVENTS] cannot inject events into the system.
+     *
+     * This test delivers [INTENT_ACTION_TEST_INJECTION] to the test app, which will attempt to
+     * inject input events, and report back to this test through the callbacks. The app should
+     * not be able to perform event injection successfully.
+     */
+    @Test
+    fun testCannotInjectInputFromApplications() {
+        val injectionTestResultLatch = CountDownLatch(1)
+        val callbacks = withCallbacks(onTestResult = { errors: List<String?> ->
+                if (errors.isNotEmpty()) {
+                    fail(errors.joinToString("\n"))
+                }
+                injectionTestResultLatch.countDown()
+            })
+        startInjectionActivitySync(callbacks).use {
+            targetContext.startActivity(Intent().apply {
+                `package` = INPUT_INJECTION_PACKAGE
+                action = INTENT_ACTION_TEST_INJECTION
+                // The NEW_TASK flag is required for starting exported activities from other
+                // processes. However, since it is a "singleInstance" activity, a new intent will
+                // be delivered to the running instance.
+                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            })
+            assertTrue(injectionTestResultLatch.await(10, TimeUnit.SECONDS),
+                "Did not receive callback for test completion")
+        }
+    }
+
+    /**
+     * When injecting pointer events through [Instrumentation.sendPointerSync], the pointer event
+     * should only be injected successfully if it is directed at a window owned by same uid as the
+     * instrumentation. This means tests cannot inject pointer events into other foreground windows
+     * that are not being instrumented.
+     */
+    @Test
+    fun testCannotInjectPointerEventsFromInstrumentationToUnownedApp() {
+        startInjectionActivitySync(withCallbacks()).use {
+            clickInCenterOfInjectionActivity { eventToInject ->
+                // The Instrumentation class should not be allowed to inject the start of a new
+                // gesture to a window owned by another uid. However, it should be allowed to inject
+                // the end of the gesture to ensure consistency.
+                try {
+                    instrumentation.sendPointerSync(eventToInject)
+                    if (eventToInject.actionMasked == MotionEvent.ACTION_DOWN) {
+                        fail("Instrumentation cannot inject DOWN to windows owned by another uid")
+                    }
+                } catch (e: RuntimeException) {
+                    if (eventToInject.actionMasked == MotionEvent.ACTION_UP) {
+                        fail("Instrumentation must be allowed to inject UP to ensure consistency")
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Instrumented tests can inject key events synchronously into any focused window through
+     * [Instrumentation].
+     */
+    @Test
+    fun testInjectKeyEventsFromInstrumentation() {
+        val keyPressLatch = CountDownLatch(2)
+        startInjectionActivitySync(
+                withCallbacks(onKey = { keyPressLatch.countDown() })).use {
+            instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_A)
+            assertEquals(0, keyPressLatch.count,
+                "Instrumentation should synchronously send key events to the activity")
+        }
+    }
+
+    /**
+     * Instrumented tests can inject pointer events synchronously into any focused window through
+     * [android.app.UiAutomation].
+     */
+    @Test
+    fun testInjectPointerEventsFromUiAutomation() {
+        val clickLatch = CountDownLatch(2)
+        startInjectionActivitySync(
+                withCallbacks(onTouch = { clickLatch.countDown() })).use {
+            clickInCenterOfInjectionActivity { eventToInject ->
+                instrumentation.uiAutomation.injectInputEvent(eventToInject, true /*sync*/)
+            }
+            assertEquals(0, clickLatch.count,
+                "UiAutomation should synchronously send pointer events to the activity")
+        }
+    }
+
+    /**
+     * Instrumented tests can inject key events synchronously into any focused window through
+     * [android.app.UiAutomation].
+     */
+    @Test
+    fun testInjectKeyEventsFromUiAutomation() {
+        val keyPressLatch = CountDownLatch(2)
+        startInjectionActivitySync(
+                withCallbacks(onKey = { keyPressLatch.countDown() })).use {
+            val downTime = SystemClock.uptimeMillis()
+            instrumentation.uiAutomation.injectInputEvent(
+                KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0),
+                true /*sync*/)
+            val upTime = SystemClock.uptimeMillis()
+            instrumentation.uiAutomation.injectInputEvent(
+                KeyEvent(downTime, upTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0),
+                true /*sync*/)
+            assertEquals(0, keyPressLatch.count,
+                "UiAutomation should synchronously send key events to the activity")
+        }
+    }
+
+    /** Synchronously starts the test activity and waits for it to gain window focus. */
+    private fun startInjectionActivitySync(callback: IInputInjectionTestCallbacks): AutoCloseable {
+        targetContext.startActivity(Intent().apply {
+            `package` = INPUT_INJECTION_PACKAGE
+            action = Intent.ACTION_MAIN
+            addCategory(Intent.CATEGORY_LAUNCHER)
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            putExtras(Bundle().also {
+                it.putBinder(INTENT_EXTRA_CALLBACK, callback.asBinder())
+            })
+        })
+
+        waitForActivityWindowFocus(INPUT_INJECTION_COMPONENT)
+
+        return AutoCloseable {
+            SystemUtil.runWithShellPermissionIdentity {
+                activityManager.forceStopPackage(INPUT_INJECTION_PACKAGE)
+            }
+        }
+    }
+
+    private fun clickInCenterOfInjectionActivity(injector: (MotionEvent) -> Unit) {
+        val bounds = getActivityBounds(INPUT_INJECTION_COMPONENT)
+        val downTime = SystemClock.uptimeMillis()
+        injector(
+            MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
+                bounds.centerX().toFloat(), bounds.centerY().toFloat(), 0))
+        val upTime = SystemClock.uptimeMillis()
+        injector(
+            MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP,
+                bounds.centerX().toFloat(), bounds.centerY().toFloat(), 0))
+    }
+}
+
+private fun withCallbacks(
+    onKey: (KeyEvent) -> Unit = InputInjectionTest.expectNoEvent,
+    onTouch: (MotionEvent) -> Unit = InputInjectionTest.expectNoEvent,
+    onTestResult: (List<String?>) -> Unit = InputInjectionTest.expectNoInjectionResult
+): IInputInjectionTestCallbacks {
+    return object : IInputInjectionTestCallbacks.Stub() {
+        override fun onKeyEvent(ev: KeyEvent?) = onKey(ev!!)
+        override fun onTouchEvent(ev: MotionEvent?) = onTouch(ev!!)
+        override fun onTestInjectionFromApp(errors: List<String?>) = onTestResult(errors)
+    }
+}
+
+private fun getActivityBounds(component: ComponentName): Rect {
+    val wms = WindowManagerState().apply { computeState() }
+    val activity = wms.getActivity(component) ?: fail("Failed to get activity for $component")
+    return activity.bounds ?: fail("Failed to get bounds for activity $component")
+}
+
+private fun waitForActivityWindowFocus(component: ComponentName) {
+    val waitUntil = SystemClock.uptimeMillis() + InputInjectionTest.WAIT_FOR_FOCUS_TIMEOUT_MILLS
+    while (true) {
+        val wms = WindowManagerState().apply { computeState() }
+        if (wms.isWindowFocused(component)) return
+        check(SystemClock.uptimeMillis() <= waitUntil) {
+            "Timed out waiting for $component window to be focused"
+        }
+        Thread.sleep(100 /*millis*/)
+    }
+}
+
+private fun WindowManagerState.isWindowFocused(component: ComponentName): Boolean {
+    return focusedApp != null && focusedWindow != null &&
+            component == ComponentName.unflattenFromString(focusedApp)!! &&
+            component == ComponentName.unflattenFromString(focusedWindow)!!
+}