blob: 2353e4e6f4d8bc4ba17c0de9647b0d83c0e43498 [file] [log] [blame]
/*
* Copyright (C) 2018 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.cts.input;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.util.ArrayMap;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Parse json resource file that contains the test commands for HidDevice
*
* For files containing reports and input events, each entry should be in the following format:
* <code>
* {"name": "test case name",
* "reports": reports,
* "events": input_events
* }
* </code>
*
* {@code reports} - an array of strings that contain hex arrays.
* {@code input_events} - an array of dicts in the following format:
* <code>
* {"action": "down|move|up", "axes": {"axis_x": x, "axis_y": y}, "keycode": "button_a"}
* </code>
* {@code "axes"} should only be defined for motion events, and {@code "keycode"} for key events.
* Timestamps will not be checked.
* Example:
* <code>
* [{ "name": "press button A",
* "reports": ["report1",
* "report2",
* "report3"
* ],
* "events": [{"action": "down", "axes": {"axis_y": 0.5, "axis_x": 0.1}},
* {"action": "move", "axes": {"axis_y": 0.0, "axis_x": 0.0}}
* ]
* },
* ... more tests like that
* ]
* </code>
*/
public class InputJsonParser {
private static final String TAG = "InputJsonParser";
private Context mContext;
public InputJsonParser(Context context) {
mContext = context;
}
/**
* Convenience function to create JSONArray from resource.
* The resource specified should contain JSON array as the top-level structure.
*
* @param resourceId The resourceId that contains the json data (typically inside R.raw)
*/
private JSONArray getJsonArrayFromResource(int resourceId) {
String data = readRawResource(resourceId);
try {
return new JSONArray(data);
} catch (JSONException e) {
throw new RuntimeException(
"Could not parse resource " + resourceId + ", received: " + data);
}
}
/**
* Convenience function to read in an entire file as a String.
*
* @param id resourceId of the file
* @return contents of the raw resource file as a String
*/
private String readRawResource(int id) {
InputStream inputStream = mContext.getResources().openRawResource(id);
try {
return readFully(inputStream);
} catch (IOException e) {
throw new RuntimeException("Could not read resource id " + id);
}
}
/**
* Read register command from raw resource.
*
* @param resourceId the raw resource id that contains the command
* @return the command to register device that can be passed to HidDevice constructor
*/
public String readRegisterCommand(int resourceId) {
return readRawResource(resourceId);
}
/**
* Read entire input stream until no data remains.
*
* @param inputStream
* @return content of the input stream
* @throws IOException
*/
private String readFully(InputStream inputStream) throws IOException {
OutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = inputStream.read(buffer);
while (read >= 0) {
baos.write(buffer, 0, read);
read = inputStream.read(buffer);
}
return baos.toString();
}
/**
* Extract the device id from the raw resource file. This is needed in order to register
* a HidDevice.
*
* @param resourceId resource file that contains the register command.
* @return hid device id
*/
public int readDeviceId(int resourceId) {
try {
JSONObject json = new JSONObject(readRawResource(resourceId));
return json.getInt("id");
} catch (JSONException e) {
throw new RuntimeException("Could not read device id from resource " + resourceId);
}
}
/**
* Extract the Vendor id from the raw resource file.
*
* @param resourceId resource file that contains the register command.
* @return device vendor id
*/
public int readVendorId(int resourceId) {
try {
JSONObject json = new JSONObject(readRawResource(resourceId));
return json.getInt("vid");
} catch (JSONException e) {
throw new RuntimeException("Could not read vendor id from resource " + resourceId);
}
}
/**
* Extract the Product id from the raw resource file.
*
* @param resourceId resource file that contains the register command.
* @return device product id
*/
public int readProductId(int resourceId) {
try {
JSONObject json = new JSONObject(readRawResource(resourceId));
return json.getInt("pid");
} catch (JSONException e) {
throw new RuntimeException("Could not read prduct id from resource " + resourceId);
}
}
private InputEvent parseInputEvent(int testCaseNumber, int source, JSONObject entry) {
try {
InputEvent event;
if (entry.has("keycode")) {
event = parseKeyEvent(source, entry);
} else if (entry.has("axes")) {
event = parseMotionEvent(source, entry);
} else {
throw new RuntimeException(
"Input event is not specified correctly. Received: " + entry);
}
return event;
} catch (JSONException e) {
throw new RuntimeException("Could not process entry " + testCaseNumber + " : " + entry);
}
}
/**
* Read json resource, and return a {@code List} of HidTestData, which contains
* the name of each test, along with the HID reports and the expected input events.
*/
public List<HidTestData> getHidTestData(int resourceId) {
JSONArray json = getJsonArrayFromResource(resourceId);
List<HidTestData> tests = new ArrayList<HidTestData>();
for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
HidTestData testData = new HidTestData();
try {
JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
testData.name = testcaseEntry.getString("name");
JSONArray reports = testcaseEntry.getJSONArray("reports");
for (int i = 0; i < reports.length(); i++) {
String report = reports.getString(i);
testData.reports.add(report);
}
final int source = sourceFromString(testcaseEntry.optString("source"));
JSONArray events = testcaseEntry.getJSONArray("events");
for (int i = 0; i < events.length(); i++) {
testData.events.add(parseInputEvent(i, source, events.getJSONObject(i)));
}
tests.add(testData);
} catch (JSONException e) {
throw new RuntimeException("Could not process entry " + testCaseNumber);
}
}
return tests;
}
/**
* Read json resource, and return a {@code List} of HidVibratorTestData, which contains
* the vibrator FF effect strength data index, and the hid output verification data.
*/
public List<HidVibratorTestData> getHidVibratorTestData(int resourceId) {
JSONArray json = getJsonArrayFromResource(resourceId);
List<HidVibratorTestData> tests = new ArrayList<HidVibratorTestData>();
for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
HidVibratorTestData testData = new HidVibratorTestData();
try {
JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
testData.leftFfIndex = testcaseEntry.getInt("leftFfIndex");
testData.rightFfIndex = testcaseEntry.getInt("rightFfIndex");
JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
JSONArray amplitudesArray = testcaseEntry.getJSONArray("amplitudes");
assertTrue(durationsArray.length() == amplitudesArray.length());
testData.durations = new ArrayList<Long>();
testData.amplitudes = new ArrayList<Integer>();
for (int i = 0; i < durationsArray.length(); i++) {
testData.durations.add(durationsArray.getLong(i));
testData.amplitudes.add(amplitudesArray.getInt(i));
}
JSONArray outputArray = testcaseEntry.getJSONArray("output");
testData.verifyMap = new ArrayMap<Integer, Integer>();
for (int i = 0; i < outputArray.length(); i++) {
JSONObject item = outputArray.getJSONObject(i);
int index = item.getInt("index");
int data = item.getInt("data");
testData.verifyMap.put(index, data);
}
tests.add(testData);
} catch (JSONException e) {
throw new RuntimeException("Could not process entry " + testCaseNumber);
}
}
return tests;
}
/**
* Read json resource, and return a {@code List} of UinputVibratorTestData, which contains
* the vibrator FF effect of durations and amplitudes.
*/
public List<UinputVibratorTestData> getUinputVibratorTestData(int resourceId) {
JSONArray json = getJsonArrayFromResource(resourceId);
List<UinputVibratorTestData> tests = new ArrayList<UinputVibratorTestData>();
for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
UinputVibratorTestData testData = new UinputVibratorTestData();
try {
JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
JSONArray durationsArray = testcaseEntry.getJSONArray("durations");
JSONArray amplitudesArray = testcaseEntry.getJSONArray("amplitudes");
assertTrue(durationsArray.length() == amplitudesArray.length());
testData.durations = new ArrayList<Long>();
testData.amplitudes = new ArrayList<Integer>();
for (int i = 0; i < durationsArray.length(); i++) {
testData.durations.add(durationsArray.getLong(i));
testData.amplitudes.add(amplitudesArray.getInt(i));
}
tests.add(testData);
} catch (JSONException e) {
throw new RuntimeException("Could not process entry " + testCaseNumber);
}
}
return tests;
}
/**
* Read json resource, and return a {@code List} of UinputTestData, which contains
* the name of each test, along with the uinput injections and the expected input events.
*/
public List<UinputTestData> getUinputTestData(int resourceId) {
JSONArray json = getJsonArrayFromResource(resourceId);
List<UinputTestData> tests = new ArrayList<UinputTestData>();
for (int testCaseNumber = 0; testCaseNumber < json.length(); testCaseNumber++) {
UinputTestData testData = new UinputTestData();
try {
JSONObject testcaseEntry = json.getJSONObject(testCaseNumber);
testData.name = testcaseEntry.getString("name");
JSONArray reports = testcaseEntry.getJSONArray("injections");
for (int i = 0; i < reports.length(); i++) {
String injections = reports.getString(i);
testData.evdevEvents.add(injections);
}
final int source = sourceFromString(testcaseEntry.optString("source"));
JSONArray events = testcaseEntry.getJSONArray("events");
for (int i = 0; i < events.length(); i++) {
testData.events.add(parseInputEvent(i, source, events.getJSONObject(i)));
}
tests.add(testData);
} catch (JSONException e) {
throw new RuntimeException("Could not process entry " + testCaseNumber);
}
}
return tests;
}
private KeyEvent parseKeyEvent(int source, JSONObject entry) throws JSONException {
int action = keyActionFromString(entry.getString("action"));
int keyCode = KeyEvent.keyCodeFromString(entry.getString("keycode"));
int metaState = metaStateFromString(entry.optString("metaState"));
// We will only check select fields of the KeyEvent. Times are not checked.
return new KeyEvent(/* downTime */ 0, /* eventTime */ 0, action, keyCode,
/* repeat */ 0, metaState, /* deviceId */ 0, /* scanCode */ 0,
/* flags */ 0, source);
}
private MotionEvent parseMotionEvent(int source, JSONObject entry) throws JSONException {
MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[1];
properties[0] = new MotionEvent.PointerProperties();
properties[0].id = 0;
properties[0].toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[1];
coords[0] = new MotionEvent.PointerCoords();
JSONObject axes = entry.getJSONObject("axes");
Iterator<String> keys = axes.keys();
while (keys.hasNext()) {
String axis = keys.next();
float value = (float) axes.getDouble(axis);
coords[0].setAxisValue(MotionEvent.axisFromString(axis), value);
}
int buttonState = 0;
JSONArray buttons = entry.optJSONArray("buttonState");
if (buttons != null) {
for (int i = 0; i < buttons.length(); ++i) {
buttonState |= motionButtonFromString(buttons.getString(i));
}
}
int action = motionActionFromString(entry.getString("action"));
// Only care about axes, action and source here. Times are not checked.
return MotionEvent.obtain(/* downTime */ 0, /* eventTime */ 0, action,
/* pointercount */ 1, properties, coords, 0, buttonState, 0f, 0f,
0, 0, source, 0);
}
private static int keyActionFromString(String action) {
switch (action.toUpperCase()) {
case "DOWN":
return KeyEvent.ACTION_DOWN;
case "UP":
return KeyEvent.ACTION_UP;
}
throw new RuntimeException("Unknown action specified: " + action);
}
private static int metaStateFromString(String metaStateString) {
int metaState = 0;
if (metaStateString.isEmpty()) {
return metaState;
}
final String[] metaKeys = metaStateString.split("\\|");
for (final String metaKeyString : metaKeys) {
final String trimmedKeyString = metaKeyString.trim();
switch (trimmedKeyString.toUpperCase()) {
case "SHIFT_LEFT":
metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
break;
case "SHIFT_RIGHT":
metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_RIGHT_ON;
break;
case "CTRL_LEFT":
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
break;
case "CTRL_RIGHT":
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_RIGHT_ON;
break;
case "ALT_LEFT":
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
break;
case "ALT_RIGHT":
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
break;
case "META_LEFT":
metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_LEFT_ON;
break;
case "META_RIGHT":
metaState |= KeyEvent.META_META_ON | KeyEvent.META_META_RIGHT_ON;
break;
case "CAPS_LOCK":
metaState |= KeyEvent.META_CAPS_LOCK_ON;
break;
case "NUM_LOCK":
metaState |= KeyEvent.META_NUM_LOCK_ON;
break;
case "SCROLL_LOCK":
metaState |= KeyEvent.META_SCROLL_LOCK_ON;
break;
default:
throw new RuntimeException("Unknown meta state chunk: " + trimmedKeyString
+ " in meta state string: " + metaStateString);
}
}
return metaState;
}
private static int motionActionFromString(String action) {
switch (action.toUpperCase()) {
case "DOWN":
return MotionEvent.ACTION_DOWN;
case "MOVE":
return MotionEvent.ACTION_MOVE;
case "UP":
return MotionEvent.ACTION_UP;
case "BUTTON_PRESS":
return MotionEvent.ACTION_BUTTON_PRESS;
case "BUTTON_RELEASE":
return MotionEvent.ACTION_BUTTON_RELEASE;
case "HOVER_ENTER":
return MotionEvent.ACTION_HOVER_ENTER;
case "HOVER_MOVE":
return MotionEvent.ACTION_HOVER_MOVE;
case "HOVER_EXIT":
return MotionEvent.ACTION_HOVER_EXIT;
}
throw new RuntimeException("Unknown action specified: " + action);
}
private static int sourceFromString(String sourceString) {
if (sourceString.isEmpty()) {
return InputDevice.SOURCE_UNKNOWN;
}
int source = 0;
final String[] sourceEntries = sourceString.split("\\|");
for (final String sourceEntry : sourceEntries) {
final String trimmedSourceEntry = sourceEntry.trim();
switch (trimmedSourceEntry.toUpperCase()) {
case "MOUSE_RELATIVE":
source |= InputDevice.SOURCE_MOUSE_RELATIVE;
break;
case "JOYSTICK":
source |= InputDevice.SOURCE_JOYSTICK;
break;
case "KEYBOARD":
source |= InputDevice.SOURCE_KEYBOARD;
break;
case "GAMEPAD":
source |= InputDevice.SOURCE_GAMEPAD;
break;
case "DPAD":
source |= InputDevice.SOURCE_DPAD;
break;
default:
throw new RuntimeException("Unknown source chunk: " + trimmedSourceEntry
+ " in source string: " + sourceString);
}
}
return source;
}
private static int motionButtonFromString(String button) {
switch (button.toUpperCase()) {
case "BACK":
return MotionEvent.BUTTON_BACK;
case "FORWARD":
return MotionEvent.BUTTON_FORWARD;
case "PRIMARY":
return MotionEvent.BUTTON_PRIMARY;
case "SECONDARY":
return MotionEvent.BUTTON_SECONDARY;
case "STYLUS_PRIMARY":
return MotionEvent.BUTTON_STYLUS_PRIMARY;
case "STYLUS_SECONDARY":
return MotionEvent.BUTTON_STYLUS_SECONDARY;
case "TERTIARY":
return MotionEvent.BUTTON_TERTIARY;
}
throw new RuntimeException("Unknown button specified: " + button);
}
}