blob: 43dda1c71093e56e960c5c4fe7edb4cb94091d4f [file] [log] [blame]
/*
* 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.filters.FlakyTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.SystemUtil
import com.android.test.inputinjection.IInputInjectionTestCallbacks
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/**
* 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 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
private lateinit var windowFocusLatch: CountDownLatch
@Before
fun setUp() {
instrumentation = InstrumentationRegistry.getInstrumentation()!!
targetContext = instrumentation.targetContext
activityManager = instrumentation.context
.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
windowFocusLatch = CountDownLatch(1)
}
/**
* 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.
*/
@FlakyTest(bugId = 293575644)
@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 {
component = INPUT_INJECTION_COMPONENT
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtras(Bundle().also {
it.putBinder(INTENT_EXTRA_CALLBACK, callback.asBinder())
})
})
assertTrue(windowFocusLatch.await(5, TimeUnit.SECONDS),
"Timed out waiting for $INPUT_INJECTION_COMPONENT window to be focused")
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)
override fun onWindowFocused() {
windowFocusLatch.countDown()
}
}
}
}
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")
}