blob: 3d940df23819f7e48f890f022b596c6e10f933a5 [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.Manifest
import android.hardware.input.InputManager
import android.provider.Settings
import android.view.InputDevice
import android.view.KeyEvent
import androidx.test.ext.junit.rules.ActivityScenarioRule
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.compatibility.common.util.ThrowingSupplier
import com.android.cts.input.UinputDevice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
private fun injectEvents(device: UinputDevice, events: IntArray) {
device.injectEvents(events.joinToString(prefix = "[", postfix = "]", separator = ","))
}
private fun injectKeyDown(device: UinputDevice, scanCode: Int) {
injectEvents(device, intArrayOf(1, scanCode, 1, 0, 0, 0))
}
private fun injectKeyUp(device: UinputDevice, scanCode: Int) {
injectEvents(device, intArrayOf(1, scanCode, 0, 0, 0, 0))
}
/**
* Create virtual keyboard devices and inject a 'hardware' key event after remapping keys. Ensure
* that the event keys are correctly remapped.
*/
@MediumTest
@RunWith(AndroidJUnit4::class)
class ModifierKeyRemappingTest {
companion object {
val REMAPPABLE_MODIFIER_KEYCODES = intArrayOf(
KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT, KeyEvent.KEYCODE_META_LEFT,
KeyEvent.KEYCODE_META_RIGHT, KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, KeyEvent.KEYCODE_CAPS_LOCK
)
// Linux keycode defined in the "linux/input-event-codes.h" header.
val KEY_LEFTALT = 56
}
private val instrumentation = InstrumentationRegistry.getInstrumentation()
@get:Rule
val rule = ActivityScenarioRule<CaptureEventActivity>(CaptureEventActivity::class.java)
private lateinit var activity: CaptureEventActivity
private lateinit var inputManager: InputManager
@Before
fun setUp() {
rule.getScenario().onActivity {
inputManager = it.getSystemService(InputManager::class.java)
activity = it
}
PollingCheck.waitFor { activity.hasWindowFocus() }
// Clear any existing remappings
clearAllModifierKeyRemappings()
// Wait for handler to execute and clear all remappings
PollingCheck.waitFor { getModifierKeyRemapping().isEmpty() }
}
private fun assertReceivedEventsCorrectlyMapped(numEvents: Int, expectedKeyCode: Int) {
for (i in 1..numEvents) {
val lastInputEvent: KeyEvent = activity.getInputEvent() as KeyEvent
assertNotNull("Event number $i is null!", lastInputEvent)
assertEquals(
"Key code should be " + KeyEvent.keyCodeToString(expectedKeyCode),
expectedKeyCode,
lastInputEvent.keyCode
)
}
activity.assertNoEvents()
}
/**
* Test modifier key remapping APIs
*/
@Test
fun testModifierKeyRemapping() {
ModifierRemappingFlag(true).use {
val keyboardDevice = UinputDevice.create(
instrumentation, R.raw.test_keyboard_register,
InputDevice.SOURCE_KEYBOARD
)
// Wait for device to be added
PollingCheck.waitFor { inputManager.getInputDevice(keyboardDevice.deviceId) != null }
val inputDevice = inputManager.getInputDevice(keyboardDevice.deviceId)
val numKeys = REMAPPABLE_MODIFIER_KEYCODES.size
// Remap modifier keys in cyclic manner
for (i in 0 until numKeys) {
remapModifierKey(
REMAPPABLE_MODIFIER_KEYCODES[i],
REMAPPABLE_MODIFIER_KEYCODES[(i + 1) % numKeys]
)
}
// Wait for handler to execute and add remappings
PollingCheck.waitFor { getModifierKeyRemapping().size == numKeys }
val remapping: Map<Int, Int> = getModifierKeyRemapping()
for (i in 0 until numKeys) {
val fromKeyCode = REMAPPABLE_MODIFIER_KEYCODES[i]
val toKeyCode = REMAPPABLE_MODIFIER_KEYCODES[(i + 1) % numKeys]
val actualToKeyCode = remapping[fromKeyCode]!!
assertEquals(
"Modifier key remapping should map " + KeyEvent.keyCodeToString(fromKeyCode) +
" to " + KeyEvent.keyCodeToString(toKeyCode) + " but was " +
KeyEvent.keyCodeToString(actualToKeyCode), toKeyCode, actualToKeyCode
)
assertEquals(
"Key location" + KeyEvent.keyCodeToString(fromKeyCode) + " should map to " +
KeyEvent.keyCodeToString(toKeyCode) + " after remapping.", toKeyCode,
inputDevice?.getKeyCodeForKeyLocation(fromKeyCode)
)
}
clearAllModifierKeyRemappings()
// Wait for handler to execute and clear all remappings
PollingCheck.waitFor { getModifierKeyRemapping().isEmpty() }
for (i in 0 until numKeys) {
val keyCode = REMAPPABLE_MODIFIER_KEYCODES[i]
assertEquals(
"Key location" + KeyEvent.keyCodeToString(keyCode) + " should map to " +
KeyEvent.keyCodeToString(keyCode) + " after remapping.", keyCode,
inputDevice?.getKeyCodeForKeyLocation(keyCode)
)
}
// Remove the device
keyboardDevice.close()
}
}
@Test
fun testHardwareKeyEventsWithRemapping_AfterKeyboardAdded() {
ModifierRemappingFlag(true).use {
val keyboardDevice = UinputDevice.create(
instrumentation, R.raw.test_keyboard_register,
InputDevice.SOURCE_KEYBOARD
)
// Wait for device to be added
PollingCheck.waitFor { inputManager.getInputDevice(keyboardDevice.deviceId) != null }
remapModifierKey(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT)
// Wait for handler to execute and add remappings
PollingCheck.waitFor { getModifierKeyRemapping().size == 1 }
activity.assertNoEvents()
injectKeyDown(keyboardDevice, KEY_LEFTALT)
injectKeyUp(keyboardDevice, KEY_LEFTALT)
assertReceivedEventsCorrectlyMapped(2, KeyEvent.KEYCODE_SHIFT_LEFT)
clearAllModifierKeyRemappings()
// Wait for handler to execute and clear all remappings
PollingCheck.waitFor { getModifierKeyRemapping().isEmpty() }
injectKeyDown(keyboardDevice, KEY_LEFTALT)
injectKeyUp(keyboardDevice, KEY_LEFTALT)
assertReceivedEventsCorrectlyMapped(2, KeyEvent.KEYCODE_ALT_LEFT)
// Remove the device
keyboardDevice.close()
}
}
@Test
fun testHardwareKeyEventsWithRemapping_BeforeKeyboardAdded() {
ModifierRemappingFlag(true).use {
remapModifierKey(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT)
// Wait for handler to execute and add remappings
PollingCheck.waitFor { getModifierKeyRemapping().size == 1 }
val keyboardDevice = UinputDevice.create(
instrumentation, R.raw.test_keyboard_register,
InputDevice.SOURCE_KEYBOARD
)
// Wait for device to be added
PollingCheck.waitFor { inputManager.getInputDevice(keyboardDevice.deviceId) != null }
activity.assertNoEvents()
injectKeyDown(keyboardDevice, KEY_LEFTALT)
injectKeyUp(keyboardDevice, KEY_LEFTALT)
assertReceivedEventsCorrectlyMapped(2, KeyEvent.KEYCODE_SHIFT_LEFT)
clearAllModifierKeyRemappings()
// Wait for handler to execute and clear all remappings
PollingCheck.waitFor { getModifierKeyRemapping().isEmpty() }
injectKeyDown(keyboardDevice, KEY_LEFTALT)
injectKeyUp(keyboardDevice, KEY_LEFTALT)
assertReceivedEventsCorrectlyMapped(2, KeyEvent.KEYCODE_ALT_LEFT)
// Remove the device
keyboardDevice.close()
}
}
/**
* Remaps a modifier key to another modifier key
*
* @param fromKey modifier key getting remapped
* @param toKey modifier key that it is getting remapped to
*/
private fun remapModifierKey(fromKey: Int, toKey: Int) {
SystemUtil.runWithShellPermissionIdentity(
{ inputManager.remapModifierKey(fromKey, toKey) },
Manifest.permission.REMAP_MODIFIER_KEYS
)
}
/**
* Clears remapping for a modifier key
*/
private fun clearAllModifierKeyRemappings() {
SystemUtil.runWithShellPermissionIdentity(
{ inputManager.clearAllModifierKeyRemappings() },
Manifest.permission.REMAP_MODIFIER_KEYS
)
}
private fun getModifierKeyRemapping(): Map<Int, Int> {
return SystemUtil.runWithShellPermissionIdentity(
ThrowingSupplier<Map<Int, Int>> { inputManager.modifierKeyRemapping },
Manifest.permission.REMAP_MODIFIER_KEYS
)
}
private inner class ModifierRemappingFlag constructor(enabled: Boolean) : AutoCloseable {
init {
Settings.Global.putString(
activity.contentResolver,
"settings_new_keyboard_modifier_key", enabled.toString()
)
}
override fun close() {
Settings.Global.putString(
activity.contentResolver,
"settings_new_keyboard_modifier_key",
""
)
}
}
}