blob: 8599a862b0530610bb826edd5a611b842d3da500 [file] [log] [blame]
/*
* Copyright (C) 2023 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.StatusBarManager
import android.graphics.Point
import android.input.cts.VirtualDisplayActivityScenarioRule.Companion.HEIGHT
import android.input.cts.VirtualDisplayActivityScenarioRule.Companion.WIDTH
import android.util.Size
import android.view.InputDevice.SOURCE_KEYBOARD
import android.view.InputDevice.SOURCE_STYLUS
import android.view.InputDevice.SOURCE_TOUCHSCREEN
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.PollingCheck
import com.android.compatibility.common.util.SystemUtil
import com.android.cts.input.UinputDevice
import com.android.cts.input.UinputTouchDevice
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Create a virtual device that supports stylus buttons, and ensure that interactions with those
* stylus buttons are sent to apps as [MotionEvent]s or system as [KeyEvent]s when stylus buttons
* are enabled.
*/
@MediumTest
@RunWith(AndroidJUnit4::class)
class StylusButtonInputEventTest {
private companion object {
// The settings namespace and key for enabling stylus button interactions.
const val SETTING_NAMESPACE_KEY = "secure stylus_buttons_enabled"
const val EV_SYN = 0
const val SYN_REPORT = 0
const val EV_KEY = 1
const val KEY_DOWN = 1
const val KEY_UP = 0
val INITIAL_SYSTEM_KEY = KeyEvent.KEYCODE_UNKNOWN
val LINUX_TO_ANDROID_KEYCODE_MAP =
mapOf<Int /* Linux keycode */, Int /* Android keycode */>(
0x14b to KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY, // BTN_STYLUS
0x14c to KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY, // BTN_STYLUS2
0x149 to KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY, // BTN_STYLUS3
)
val LINUX_KEYCODE_TO_MOTIONEVENT_BUTTON =
mapOf<Int, Int>(
0x14b to MotionEvent.BUTTON_STYLUS_PRIMARY, // BTN_STYLUS
0x14c to MotionEvent.BUTTON_STYLUS_SECONDARY, // BTN_STYLUS2
)
}
@get:Rule val virtualDisplayRule = VirtualDisplayActivityScenarioRule()
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private lateinit var statusBarManager: StatusBarManager
private lateinit var initialStylusButtonsEnabledSetting: String
@Before
fun setUp() {
initialStylusButtonsEnabledSetting =
SystemUtil.runShellCommandOrThrow("settings get $SETTING_NAMESPACE_KEY")
statusBarManager =
instrumentation.targetContext.getSystemService(StatusBarManager::class.java)
// Send an unrelated system key to the status bar so last stylus system key history is not
// preserved between tests.
SystemUtil.runWithShellPermissionIdentity {
statusBarManager.handleSystemKey(KeyEvent(KeyEvent.ACTION_DOWN, INITIAL_SYSTEM_KEY))
}
}
@After
fun tearDown() {
SystemUtil.runShellCommandOrThrow(
"settings put $SETTING_NAMESPACE_KEY $initialStylusButtonsEnabledSetting"
)
}
@Test
fun testStylusButtonsEnabledKeyEvents() {
enableStylusButtons()
UinputDevice.create(
instrumentation,
R.raw.test_bluetooth_stylus_register,
SOURCE_KEYBOARD or SOURCE_STYLUS
).use { bluetoothStylus ->
for (button in LINUX_TO_ANDROID_KEYCODE_MAP.entries.iterator()) {
bluetoothStylus.injectEvents(
makeEvents(EV_KEY, button.key, KEY_DOWN, EV_SYN, SYN_REPORT, 0)
)
// The stylus button is expected to be sent to the status bar as a system key on
// the down press.
assertReceivedSystemKey(button.value)
bluetoothStylus.injectEvents(
makeEvents(EV_KEY, button.key, KEY_UP, EV_SYN, SYN_REPORT, 0)
)
}
}
}
@Test
fun testStylusButtonsDisabledKeyEvents() {
disableStylusButtons()
UinputDevice.create(
instrumentation,
R.raw.test_bluetooth_stylus_register,
SOURCE_KEYBOARD or SOURCE_STYLUS
).use { bluetoothStylus ->
for (button in LINUX_TO_ANDROID_KEYCODE_MAP.entries.iterator()) {
bluetoothStylus.injectEvents(
makeEvents(EV_KEY, button.key, KEY_DOWN, EV_SYN, SYN_REPORT, 0)
)
bluetoothStylus.injectEvents(
makeEvents(EV_KEY, button.key, KEY_UP, EV_SYN, SYN_REPORT, 0)
)
// Stylus buttons should not be sent to the status bar as a system key when
// stylus buttons are disabled.
assertNoSystemKey()
}
}
}
@Test
fun testStylusButtonsEnabledMotionEvents() {
enableStylusButtons()
UinputTouchDevice(
instrumentation,
virtualDisplayRule.virtualDisplay.display,
Size(WIDTH, HEIGHT),
R.raw.test_capacitive_stylus_register,
SOURCE_TOUCHSCREEN,
).use { uinputStylus ->
val pointer = Point(100, 100)
for (button in LINUX_KEYCODE_TO_MOTIONEVENT_BUTTON.entries.iterator()) {
pointer.offset(1, 1)
uinputStylus.sendBtnTouch(true)
uinputStylus.sendBtn(button.key, true)
uinputStylus.sendDown(0, pointer, UinputTouchDevice.MT_TOOL_PEN)
assertNextMotionEventEquals(
MotionEvent.ACTION_DOWN,
MotionEvent.TOOL_TYPE_STYLUS,
button.value,
0,
SOURCE_STYLUS,
)
assertNextMotionEventEquals(
MotionEvent.ACTION_BUTTON_PRESS,
MotionEvent.TOOL_TYPE_STYLUS,
button.value,
button.value,
SOURCE_STYLUS,
)
uinputStylus.sendBtnTouch(false)
uinputStylus.sendBtn(button.key, false)
uinputStylus.sendUp(0)
assertNextMotionEventEquals(
MotionEvent.ACTION_BUTTON_RELEASE,
MotionEvent.TOOL_TYPE_STYLUS,
0,
button.value,
SOURCE_STYLUS,
)
assertNextMotionEventEquals(
MotionEvent.ACTION_UP,
MotionEvent.TOOL_TYPE_STYLUS,
0,
0,
SOURCE_STYLUS,
)
}
}
}
@Test
fun testStylusButtonsDisabledMotionEvents() {
disableStylusButtons()
UinputTouchDevice(
instrumentation,
virtualDisplayRule.virtualDisplay.display,
Size(WIDTH, HEIGHT),
R.raw.test_capacitive_stylus_register,
SOURCE_TOUCHSCREEN,
).use { uinputStylus ->
val pointer = Point(100, 100)
for (button in LINUX_KEYCODE_TO_MOTIONEVENT_BUTTON.entries.iterator()) {
pointer.offset(1, 1)
uinputStylus.sendBtnTouch(true)
uinputStylus.sendBtn(button.key, true)
uinputStylus.sendDown(0, pointer, UinputTouchDevice.MT_TOOL_PEN)
assertNextMotionEventEquals(
MotionEvent.ACTION_DOWN,
MotionEvent.TOOL_TYPE_STYLUS,
0,
0,
SOURCE_STYLUS,
)
uinputStylus.sendBtnTouch(false)
uinputStylus.sendBtn(button.key, false)
uinputStylus.sendUp(0)
assertNextMotionEventEquals(
MotionEvent.ACTION_UP,
MotionEvent.TOOL_TYPE_STYLUS,
0,
0,
SOURCE_STYLUS,
)
}
}
}
private fun assertReceivedSystemKey(keycode: Int) {
SystemUtil.runWithShellPermissionIdentity {
PollingCheck.waitFor { statusBarManager.lastSystemKey == keycode }
}
}
private fun assertNoSystemKey() {
// Wait for the system to process the event.
Thread.sleep(100)
SystemUtil.runWithShellPermissionIdentity {
assertEquals(INITIAL_SYSTEM_KEY, statusBarManager.lastSystemKey)
}
}
private fun assertNextMotionEventEquals(
action: Int,
toolType: Int,
buttonState: Int,
actionButton: Int,
source: Int,
) {
val event = virtualDisplayRule.activity.getInputEvent() as MotionEvent
assertEquals(action, event.action)
assertEquals(toolType, event.getToolType(0))
assertEquals(buttonState, event.buttonState)
assertEquals(actionButton, event.actionButton)
assertEquals(source and event.source, source)
}
private fun enableStylusButtons() {
SystemUtil.runShellCommandOrThrow("settings put $SETTING_NAMESPACE_KEY 1")
}
private fun disableStylusButtons() {
SystemUtil.runShellCommandOrThrow("settings put $SETTING_NAMESPACE_KEY 0")
}
}
private fun makeEvents(vararg codes: Int): String {
return codes.joinToString(prefix = "[", postfix = "]", separator = ",")
}