blob: ca53a1551f7ce5bfb74ad784c25be8e3f227f4f0 [file] [log] [blame]
/*
* Copyright (C) 2020 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.view.cts.input;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import android.app.Instrumentation;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.cts.R;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import com.android.compatibility.common.util.WindowUtil;
import com.android.cts.input.InputJsonParser;
import com.android.cts.input.UinputDevice;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* CTS test case for generic.kl key layout mapping.
* This test utilize uinput command line tool to create a test device, and configure the virtual
* device to have all keys need to be tested. The JSON format input for device configuration
* and EV_KEY injection will be created directly from this test for uinput command.
* Keep res/raw/Generic.kl in sync with framework/base/data/keyboards/Generic.kl, this file
* will be loaded and parsed in this test, looping through all key labels and the corresponding
* EV_KEY code, injecting the KEY_UP and KEY_DOWN event to uinput, then verify the KeyEvent
* delivered to test application view. Except meta control keys and special keys not delivered
* to apps, all key codes in generic.kl will be verified.
*
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
public class InputDeviceKeyLayoutMapTest {
private static final String TAG = "InputDeviceKeyLayoutMapTest";
private static final String LABEL_PREFIX = "KEYCODE_";
private static final int DEVICE_ID = 1;
private static final int EV_SYN = 0;
private static final int EV_KEY = 1;
private static final int EV_KEY_DOWN = 1;
private static final int EV_KEY_UP = 0;
private static final int UI_SET_EVBIT = 100;
private static final int UI_SET_KEYBIT = 101;
private static final int GOOGLE_VENDOR_ID = 0x18d1;
private static final int GOOGLE_VIRTUAL_KEYBOARD_ID = 0x001f;
private static final int POLL_EVENT_TIMEOUT_SECONDS = 1;
private static final int RETRY_COUNT = 10;
private Map<String, Integer> mKeyLayout;
private Instrumentation mInstrumentation;
private UinputDevice mUinputDevice;
private int mMetaState;
private InputJsonParser mParser;
private static native Map<String, Integer> nativeLoadKeyLayout(String genericKeyLayout);
static {
System.loadLibrary("ctsview_jni");
}
@Rule
public ActivityTestRule<InputDeviceKeyLayoutMapTestActivity> mActivityRule =
new ActivityTestRule<>(InputDeviceKeyLayoutMapTestActivity.class);
@Before
public void setup() {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
WindowUtil.waitForFocus(mActivityRule.getActivity());
mParser = new InputJsonParser(mInstrumentation.getTargetContext());
mKeyLayout = nativeLoadKeyLayout(mParser.readRegisterCommand(R.raw.Generic));
mUinputDevice = new UinputDevice(mInstrumentation, DEVICE_ID, GOOGLE_VENDOR_ID,
GOOGLE_VIRTUAL_KEYBOARD_ID, InputDevice.SOURCE_KEYBOARD,
createDeviceRegisterCommand());
mMetaState = KeyEvent.META_NUM_LOCK_ON;
}
@After
public void tearDown() {
if (mUinputDevice != null) {
mUinputDevice.close();
}
}
/**
* Get a KeyEvent from event queue or timeout.
* The test activity instance may change in the middle, calling getKeyEvent with the old
* activity instance will get timed out when test activity instance changed. Rather than
* doing a long wait for timeout with same activity instance, break the polling into a number
* of retries and each time of retry call the ActivityTestRule.getActivity for current activity
* instance to avoid the test failure because of polling the old activity instance get timed
* out consequently failed the test.
*
* @param retryCount The times to retry get KeyEvent from test activity.
*
* @return KeyEvent delivered to test activity, null if timeout.
*/
private KeyEvent getKeyEvent(int retryCount) {
for (int i = 0; i < retryCount; i++) {
KeyEvent event = mActivityRule.getActivity().getKeyEvent(POLL_EVENT_TIMEOUT_SECONDS);
if (event != null) {
return event;
}
}
return null;
}
/**
* Asserts that the application received a {@link android.view.KeyEvent} with the given
* metadata.
*
* If other KeyEvents are received by the application prior to the expected KeyEvent, or no
* KeyEvents are received within a reasonable amount of time, then this will throw an
* {@link AssertionError}.
*
* Only action, source, keyCode and metaState are being compared.
*/
private void assertReceivedKeyEvent(@NonNull KeyEvent expectedKeyEvent) {
if (expectedKeyEvent.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
return;
}
KeyEvent receivedKeyEvent = getKeyEvent(RETRY_COUNT);
String log = "Expected " + expectedKeyEvent + " Received " + receivedKeyEvent;
assertNotNull(log, receivedKeyEvent);
assertEquals(log, expectedKeyEvent.getAction(), receivedKeyEvent.getAction());
assertEquals(log, expectedKeyEvent.getSource(), receivedKeyEvent.getSource());
assertEquals(log, expectedKeyEvent.getKeyCode(), receivedKeyEvent.getKeyCode());
assertEquals(log, expectedKeyEvent.getMetaState(), receivedKeyEvent.getMetaState());
}
/**
* Create the uinput device registration command, in JSON format of uinput commandline tool.
* Refer to {@link framework/base/cmds/uinput/README.md}
*/
private String createDeviceRegisterCommand() {
JSONObject json = new JSONObject();
JSONArray arrayConfigs = new JSONArray();
try {
json.put("id", DEVICE_ID);
json.put("type", "uinput");
json.put("command", "register");
json.put("name", "Virtual All Buttons Device (Test)");
json.put("vid", GOOGLE_VENDOR_ID);
json.put("pid", GOOGLE_VIRTUAL_KEYBOARD_ID);
json.put("bus", "bluetooth");
JSONObject jsonSetEvBit = new JSONObject();
JSONArray arraySetEvBit = new JSONArray();
arraySetEvBit.put(EV_KEY);
jsonSetEvBit.put("type", UI_SET_EVBIT);
jsonSetEvBit.put("data", arraySetEvBit);
arrayConfigs.put(jsonSetEvBit);
// Configure device have all keys from key layout map.
JSONArray arraySetKeyBit = new JSONArray();
for (Map.Entry<String, Integer> entry : mKeyLayout.entrySet()) {
arraySetKeyBit.put(entry.getValue());
}
JSONObject jsonSetKeyBit = new JSONObject();
jsonSetKeyBit.put("type", UI_SET_KEYBIT);
jsonSetKeyBit.put("data", arraySetKeyBit);
arrayConfigs.put(jsonSetKeyBit);
json.put("configuration", arrayConfigs);
} catch (JSONException e) {
throw new RuntimeException(
"Could not create JSON object");
}
return json.toString();
}
/**
* Update expected meta state for incoming key event.
* @param action KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
* @param label Key label from key layout mapping definition
* @return updated meta state
*/
private int updateMetaState(int action, String label) {
int metaState = 0;
int metaStateToggle = 0;
if (label.equals("CTRL_LEFT")) {
metaState = KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
}
if (label.equals("CTRL_RIGHT")) {
metaState = KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_RIGHT_ON;
}
if (label.equals("SHIFT_LEFT")) {
metaState = KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
}
if (label.equals("SHIFT_RIGHT")) {
metaState = KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON;
}
if (label.equals("ALT_LEFT")) {
metaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
}
if (label.equals("ALT_RIGHT")) {
metaState = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
}
if (label.equals("CAPS_LOCK")) {
metaStateToggle = KeyEvent.META_CAPS_LOCK_ON;
}
if (label.equals("NUM_LOCK")) {
metaStateToggle = KeyEvent.META_NUM_LOCK_ON;
}
if (label.equals("SCROLL_LOCK")) {
metaStateToggle = KeyEvent.META_SCROLL_LOCK_ON;
}
if (action == KeyEvent.ACTION_DOWN) {
mMetaState |= metaState;
} else if (action == KeyEvent.ACTION_UP) {
mMetaState &= ~metaState;
}
if (action == KeyEvent.ACTION_UP) {
if ((mMetaState & metaStateToggle) == 0) {
mMetaState |= metaStateToggle;
} else {
mMetaState &= ~metaStateToggle;
}
}
return mMetaState;
}
/**
* Generate a key event from the key label and action.
* @param action KeyEvent.ACTION_DOWN or KeyEvent.ACTION_UP
* @param label Key label from key layout mapping definition
* @return KeyEvent expected to receive
*/
private KeyEvent generateKeyEvent(int action, String label) {
int source = InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_GAMEPAD
| InputDevice.SOURCE_DPAD;
int keyCode = KeyEvent.keyCodeFromString(LABEL_PREFIX + label);
int metaState = updateMetaState(action, label);
// We will only check select fields of the KeyEvent. Times are not checked.
KeyEvent event = new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
/* repeat */ 0, metaState, /* deviceId */ 0, /* scanCode */ 0,
/* flags */ 0, source);
return event;
}
/**
* Simulate pressing a key.
* @param evKeyCode The key scan code
*/
private void pressKey(int evKeyCode) {
int[] evCodesDown = new int[] {
EV_KEY, evKeyCode, EV_KEY_DOWN,
EV_SYN, 0, 0};
mUinputDevice.injectEvents(Arrays.toString(evCodesDown));
int[] evCodesUp = new int[] {
EV_KEY, evKeyCode, EV_KEY_UP,
EV_SYN, 0, 0 };
mUinputDevice.injectEvents(Arrays.toString(evCodesUp));
}
/**
* Check the initial global meta key state.
* @param label Key label from key layout mapping definition
* @param metaState The meta state that the meta key changes
*/
private void checkMetaKeyState(String label, int metaState) {
int eveKeyCode = mKeyLayout.get(label);
pressKey(eveKeyCode);
// Get 2 key events for up and down.
KeyEvent keyDownEvent = getKeyEvent(RETRY_COUNT);
assertNotNull("Didn't get KeyDown event " + label, keyDownEvent);
KeyEvent keyUpEvent = getKeyEvent(RETRY_COUNT);
assertNotNull("Didn't get KeyUp event " + label, keyUpEvent);
if (keyUpEvent.getKeyCode() == KeyEvent.keyCodeFromString(label)
&& keyUpEvent.getAction() == KeyEvent.ACTION_UP) {
mMetaState &= ~metaState;
mMetaState |= (keyUpEvent.getMetaState() & metaState);
}
}
/**
* Initialize NUM_LOCK, CAPS_LOCK, SCROLL_LOCK state as they are global meta state
*/
private void initializeMetaKeysState() {
// Detect NUM_LOCK key state before test.
checkMetaKeyState("NUM_LOCK", KeyEvent.META_NUM_LOCK_ON);
// Detect CAPS_LOCK key state before test.
checkMetaKeyState("CAPS_LOCK", KeyEvent.META_CAPS_LOCK_ON);
// Detect CAPS_LOCK key state before test.
checkMetaKeyState("SCROLL_LOCK", KeyEvent.META_SCROLL_LOCK_ON);
}
@Test
public void testLayoutKeyEvents() {
final List<String> excludedKeys = Arrays.asList(
// Meta control keys.
"CAPS_LOCK", "NUM_LOCK", "SCROLL_LOCK", "META_LEFT", "META_RIGHT", "FUNCTION",
// KeyEvents not delivered to apps.
"APP_SWITCH", "SYSRQ", "ASSIST", "VOICE_ASSIST",
"HOME", "POWER", "SLEEP", "WAKEUP",
"BRIGHTNESS_UP", "BRIGHTNESS_DOWN");
initializeMetaKeysState();
for (Map.Entry<String, Integer> entry : mKeyLayout.entrySet()) {
String label = LABEL_PREFIX + entry.getKey();
int evKeyCode = entry.getValue();
if (excludedKeys.contains(label)) {
continue;
}
assertNotEquals(KeyEvent.keyCodeFromString(label), KeyEvent.KEYCODE_UNKNOWN);
// Press the key
pressKey(evKeyCode);
// Generate expected key down event and verify
KeyEvent expectedDownEvent = generateKeyEvent(KeyEvent.ACTION_DOWN, label);
assertReceivedKeyEvent(expectedDownEvent);
// Generate expected key up event and verify
KeyEvent expectedUpEvent = generateKeyEvent(KeyEvent.ACTION_UP, label);
assertReceivedKeyEvent(expectedUpEvent);
}
}
}