Uinput shell command: Support string literals for evdev event codes
Using the evedev event label strings directly in the uinput device's command will make the commands much more readable and developer friendly. Bug: 293327283 Test: atest CtsInputTestsCases Change-Id: I56ac7b228613ddffae3fec5261885de54db78c2c
This commit is contained in:
@@ -59,22 +59,25 @@ multiple devices are registered.
|
||||
and `"bluetooth"`.
|
||||
|
||||
Device configuration is used to configure the uinput device. The `type` field provides a `UI_SET_*`
|
||||
control code, and data is a vector of control values to be sent to the uinput device, which depends
|
||||
on the control code.
|
||||
control code as an integer value or a string label (e.g. `"UI_SET_EVBIT"`), and data is a vector of
|
||||
control values to be sent to the uinput device, which depends on the control code.
|
||||
|
||||
| Field | Type | Description |
|
||||
|:-------------:|:-------------:|:-------------------------- |
|
||||
| `type` | integer | `UI_SET_` control type |
|
||||
| `data` | integer array | control values |
|
||||
| Field | Type | Description |
|
||||
|:-------------:|:---------------------:|:-----------------------|
|
||||
| `type` | integer\|string | `UI_SET_` control type |
|
||||
| `data` | integer\|string array | control values |
|
||||
|
||||
Due to the sequential nature in which this is parsed, the `type` field must be specified before
|
||||
the `data` field in this JSON Object.
|
||||
|
||||
`ff_effects_max` must be provided if `UI_SET_FFBIT` is used in `configuration`.
|
||||
|
||||
`abs_info` fields are provided to set the device axes information. It is an array of below objects:
|
||||
|
||||
| Field | Type | Description |
|
||||
|:-------------:|:-------------:|:-------------------------- |
|
||||
| `code` | integer | Axis code |
|
||||
| `info` | object | Axis information object |
|
||||
| Field | Type | Description |
|
||||
|:-------------:|:---------------:|:------------------------|
|
||||
| `code` | integer\|string | Axis code or label |
|
||||
| `info` | object | Axis information object |
|
||||
|
||||
The axis information object is defined as below, with the fields having the same meaning as those
|
||||
Linux's [`struct input_absinfo`][struct input_absinfo]:
|
||||
@@ -99,16 +102,17 @@ Example:
|
||||
"pid": 0x2c42,
|
||||
"bus": "usb",
|
||||
"configuration":[
|
||||
{"type":100, "data":[1, 21]}, // UI_SET_EVBIT : EV_KEY and EV_FF
|
||||
{"type":101, "data":[11, 2, 3, 4]}, // UI_SET_KEYBIT : KEY_0 KEY_1 KEY_2 KEY_3
|
||||
{"type":107, "data":[80]} // UI_SET_FFBIT : FF_RUMBLE
|
||||
{"type":"UI_SET_EVBIT", "data":["EV_KEY", "EV_FF"]},
|
||||
{"type":"UI_SET_KEYBIT", "data":["KEY_0", "KEY_1", "KEY_2", "KEY_3"]},
|
||||
{"type":"UI_SET_ABSBIT", "data":["ABS_Y", "ABS_WHEEL"]},
|
||||
{"type":"UI_SET_FFBIT", "data":["FF_RUMBLE"]}
|
||||
],
|
||||
"ff_effects_max" : 1,
|
||||
"abs_info": [
|
||||
{"code":1, "info": {"value":20, "minimum":-255,
|
||||
{"code":"ABS_Y", "info": {"value":20, "minimum":-255,
|
||||
"maximum":255, "fuzz":0, "flat":0, "resolution":1}
|
||||
},
|
||||
{"code":8, "info": {"value":-50, "minimum":-255,
|
||||
{"code":"ABS_WHEEL", "info": {"value":-50, "minimum":-255,
|
||||
"maximum":255, "fuzz":0, "flat":0, "resolution":1}
|
||||
}
|
||||
]
|
||||
@@ -157,11 +161,11 @@ Example:
|
||||
|
||||
Send an array of uinput event packets to the uinput device
|
||||
|
||||
| Field | Type | Description |
|
||||
|:-------------:|:-------------:|:-------------------------- |
|
||||
| `id` | integer | Device ID |
|
||||
| `command` | string | Must be set to "inject" |
|
||||
| `events` | integer array | events to inject |
|
||||
| Field | Type | Description |
|
||||
|:-------------:|:---------------------:|:-------------------------- |
|
||||
| `id` | integer | Device ID |
|
||||
| `command` | string | Must be set to "inject" |
|
||||
| `events` | integer\|string array | events to inject |
|
||||
|
||||
The `events` parameter is an array of integers in sets of three: a type, an axis code, and an axis
|
||||
value, like you'd find in Linux's `struct input_event`. For example, sending presses of the 0 and 1
|
||||
@@ -171,14 +175,14 @@ keys would look like this:
|
||||
{
|
||||
"id": 1,
|
||||
"command": "inject",
|
||||
"events": [0x01, 0xb, 0x1, // EV_KEY, KEY_0, DOWN
|
||||
0x00, 0x00, 0x00, // EV_SYN, SYN_REPORT, 0
|
||||
0x01, 0x0b, 0x00, // EV_KEY, KEY_0, UP
|
||||
0x00, 0x00, 0x00, // EV_SYN, SYN_REPORT, 0
|
||||
0x01, 0x2, 0x1, // EV_KEY, KEY_1, DOWN
|
||||
0x00, 0x00, 0x01, // EV_SYN, SYN_REPORT, 0
|
||||
0x01, 0x02, 0x00, // EV_KEY, KEY_1, UP
|
||||
0x00, 0x00, 0x01 // EV_SYN, SYN_REPORT, 0
|
||||
"events": ["EV_KEY", "KEY_0", 1,
|
||||
"EV_SYN", "SYN_REPORT", 0,
|
||||
"EV_KEY", "KEY_0", 0,
|
||||
"EV_SYN", "SYN_REPORT", 0,
|
||||
"EV_KEY", "KEY_1", 1,
|
||||
"EV_SYN", "SYN_REPORT", 0,
|
||||
"EV_KEY", "KEY_1", 0,
|
||||
"EV_SYN", "SYN_REPORT", 0
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -21,6 +21,7 @@ cc_library_shared {
|
||||
"libbase",
|
||||
"libbinder",
|
||||
"liblog",
|
||||
"libinput",
|
||||
"libnativehelper",
|
||||
],
|
||||
|
||||
|
||||
@@ -16,12 +16,24 @@
|
||||
|
||||
#define LOG_TAG "UinputCommandDevice"
|
||||
|
||||
#include <linux/uinput.h>
|
||||
#include "com_android_commands_uinput_Device.h"
|
||||
|
||||
#include <android-base/stringprintf.h>
|
||||
#include <android/looper.h>
|
||||
#include <android_os_Parcel.h>
|
||||
#include <fcntl.h>
|
||||
#include <input/InputEventLabels.h>
|
||||
#include <inttypes.h>
|
||||
#include <jni.h>
|
||||
#include <linux/uinput.h>
|
||||
#include <log/log.h>
|
||||
#include <nativehelper/JNIHelp.h>
|
||||
#include <nativehelper/ScopedLocalRef.h>
|
||||
#include <nativehelper/ScopedPrimitiveArray.h>
|
||||
#include <nativehelper/ScopedUtfChars.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
@@ -30,19 +42,6 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <android/looper.h>
|
||||
#include <android_os_Parcel.h>
|
||||
#include <jni.h>
|
||||
#include <log/log.h>
|
||||
#include <nativehelper/JNIHelp.h>
|
||||
#include <nativehelper/ScopedLocalRef.h>
|
||||
#include <nativehelper/ScopedPrimitiveArray.h>
|
||||
#include <nativehelper/ScopedUtfChars.h>
|
||||
|
||||
#include <android-base/stringprintf.h>
|
||||
|
||||
#include "com_android_commands_uinput_Device.h"
|
||||
|
||||
namespace android {
|
||||
namespace uinput {
|
||||
|
||||
@@ -307,6 +306,21 @@ static void setAbsInfo(JNIEnv* env, jclass /* clazz */, jint handle, jint axisCo
|
||||
::ioctl(static_cast<int>(handle), UI_ABS_SETUP, &absSetup);
|
||||
}
|
||||
|
||||
static jint getEvdevEventTypeByLabel(JNIEnv* env, jclass /* clazz */, jstring rawLabel) {
|
||||
ScopedUtfChars label(env, rawLabel);
|
||||
return InputEventLookup::getLinuxEvdevEventTypeByLabel(label.c_str()).value_or(-1);
|
||||
}
|
||||
|
||||
static jint getEvdevEventCodeByLabel(JNIEnv* env, jclass /* clazz */, jint type, jstring rawLabel) {
|
||||
ScopedUtfChars label(env, rawLabel);
|
||||
return InputEventLookup::getLinuxEvdevEventCodeByLabel(type, label.c_str()).value_or(-1);
|
||||
}
|
||||
|
||||
static jint getEvdevInputPropByLabel(JNIEnv* env, jclass /* clazz */, jstring rawLabel) {
|
||||
ScopedUtfChars label(env, rawLabel);
|
||||
return InputEventLookup::getLinuxEvdevInputPropByLabel(label.c_str()).value_or(-1);
|
||||
}
|
||||
|
||||
static JNINativeMethod sMethods[] = {
|
||||
{"nativeOpenUinputDevice",
|
||||
"(Ljava/lang/String;IIIIILjava/lang/String;"
|
||||
@@ -316,6 +330,12 @@ static JNINativeMethod sMethods[] = {
|
||||
{"nativeConfigure", "(II[I)V", reinterpret_cast<void*>(configure)},
|
||||
{"nativeSetAbsInfo", "(IILandroid/os/Parcel;)V", reinterpret_cast<void*>(setAbsInfo)},
|
||||
{"nativeCloseUinputDevice", "(J)V", reinterpret_cast<void*>(closeUinputDevice)},
|
||||
{"nativeGetEvdevEventTypeByLabel", "(Ljava/lang/String;)I",
|
||||
reinterpret_cast<void*>(getEvdevEventTypeByLabel)},
|
||||
{"nativeGetEvdevEventCodeByLabel", "(ILjava/lang/String;)I",
|
||||
reinterpret_cast<void*>(getEvdevEventCodeByLabel)},
|
||||
{"nativeGetEvdevInputPropByLabel", "(Ljava/lang/String;)I",
|
||||
reinterpret_cast<void*>(getEvdevInputPropByLabel)},
|
||||
};
|
||||
|
||||
int register_com_android_commands_uinput_Device(JNIEnv* env) {
|
||||
|
||||
@@ -66,6 +66,9 @@ public class Device {
|
||||
private static native void nativeInjectEvent(long ptr, int type, int code, int value);
|
||||
private static native void nativeConfigure(int handle, int code, int[] configs);
|
||||
private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel);
|
||||
private static native int nativeGetEvdevEventTypeByLabel(String label);
|
||||
private static native int nativeGetEvdevEventCodeByLabel(int type, String label);
|
||||
private static native int nativeGetEvdevInputPropByLabel(String label);
|
||||
|
||||
public Device(int id, String name, int vid, int pid, int bus,
|
||||
SparseArray<int[]> configuration, int ffEffectsMax,
|
||||
@@ -234,4 +237,32 @@ public class Device {
|
||||
msg.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
static int getEvdevEventTypeByLabel(String label) {
|
||||
final var type = nativeGetEvdevEventTypeByLabel(label);
|
||||
if (type < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to get evdev event type from label: " + label);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
static int getEvdevEventCodeByLabel(int type, String label) {
|
||||
final var code = nativeGetEvdevEventCodeByLabel(type, label);
|
||||
if (code < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to get evdev event code for type " + type + " from label: " + label);
|
||||
}
|
||||
return code;
|
||||
|
||||
}
|
||||
|
||||
static int getEvdevInputPropByLabel(String label) {
|
||||
final var prop = nativeGetEvdevInputPropByLabel(label);
|
||||
if (prop < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to get evdev input prop from label: " + label);
|
||||
}
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import src.com.android.commands.uinput.InputAbsInfo;
|
||||
@@ -39,7 +42,35 @@ public class Event {
|
||||
public static final String COMMAND_REGISTER = "register";
|
||||
public static final String COMMAND_DELAY = "delay";
|
||||
public static final String COMMAND_INJECT = "inject";
|
||||
private static final int ABS_CNT = 64;
|
||||
private static final int EV_KEY = 0x01;
|
||||
private static final int EV_REL = 0x02;
|
||||
private static final int EV_ABS = 0x03;
|
||||
private static final int EV_MSC = 0x04;
|
||||
private static final int EV_SW = 0x05;
|
||||
private static final int EV_LED = 0x11;
|
||||
private static final int EV_SND = 0x12;
|
||||
private static final int EV_FF = 0x15;
|
||||
|
||||
private enum UinputControlCode {
|
||||
UI_SET_EVBIT("UI_SET_EVBIT", 100),
|
||||
UI_SET_KEYBIT("UI_SET_KEYBIT", 101),
|
||||
UI_SET_RELBIT("UI_SET_RELBIT", 102),
|
||||
UI_SET_ABSBIT("UI_SET_ABSBIT", 103),
|
||||
UI_SET_MSCBIT("UI_SET_MSCBIT", 104),
|
||||
UI_SET_LEDBIT("UI_SET_LEDBIT", 105),
|
||||
UI_SET_SNDBIT("UI_SET_SNDBIT", 106),
|
||||
UI_SET_FFBIT("UI_SET_FFBIT", 107),
|
||||
UI_SET_SWBIT("UI_SET_SWBIT", 109),
|
||||
UI_SET_PROPBIT("UI_SET_PROPBIT", 110);
|
||||
|
||||
final String mName;
|
||||
final int mValue;
|
||||
|
||||
UinputControlCode(String name, int value) {
|
||||
this.mName = name;
|
||||
this.mValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
// These constants come from "include/uapi/linux/input.h" in the kernel
|
||||
enum Bus {
|
||||
@@ -257,8 +288,8 @@ public class Event {
|
||||
eb.setBus(readBus());
|
||||
break;
|
||||
case "events":
|
||||
int[] injections = readIntList().stream()
|
||||
.mapToInt(Integer::intValue).toArray();
|
||||
int[] injections = readInjectedEvents().stream()
|
||||
.mapToInt(Integer::intValue).toArray();
|
||||
eb.setInjections(injections);
|
||||
break;
|
||||
case "configuration":
|
||||
@@ -293,12 +324,17 @@ public class Event {
|
||||
return e;
|
||||
}
|
||||
|
||||
private ArrayList<Integer> readIntList() throws IOException {
|
||||
ArrayList<Integer> data = new ArrayList<Integer>();
|
||||
private ArrayList<Integer> readInjectedEvents() throws IOException {
|
||||
ArrayList<Integer> data = new ArrayList<>();
|
||||
try {
|
||||
mReader.beginArray();
|
||||
while (mReader.hasNext()) {
|
||||
data.add(Integer.decode(mReader.nextString()));
|
||||
// Read events in groups of three, because we expect an event type, event code,
|
||||
// and event value.
|
||||
final int type = readEvdevEventType();
|
||||
data.add(type);
|
||||
data.add(readEvdevEventCode(type));
|
||||
data.add(readInt());
|
||||
}
|
||||
mReader.endArray();
|
||||
} catch (IllegalStateException | NumberFormatException e) {
|
||||
@@ -309,22 +345,32 @@ public class Event {
|
||||
return data;
|
||||
}
|
||||
|
||||
private byte[] readData() throws IOException {
|
||||
ArrayList<Integer> data = readIntList();
|
||||
byte[] rawData = new byte[data.size()];
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
int d = data.get(i);
|
||||
if ((d & 0xFF) != d) {
|
||||
throw new IllegalStateException("Invalid data, all values must be byte-sized");
|
||||
private int readValueAsInt(Function<String, Integer> stringToInt) throws IOException {
|
||||
switch (mReader.peek()) {
|
||||
case NUMBER: {
|
||||
return mReader.nextInt();
|
||||
}
|
||||
case STRING: {
|
||||
final var str = mReader.nextString();
|
||||
try {
|
||||
// Attempt to first parse the value as an int.
|
||||
return Integer.decode(str);
|
||||
} catch (NumberFormatException e) {
|
||||
// Then fall back to the supplied function.
|
||||
return stringToInt.apply(str);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new IllegalStateException(
|
||||
"Encountered malformed data. Expected int or string.");
|
||||
}
|
||||
rawData[i] = (byte) d;
|
||||
}
|
||||
return rawData;
|
||||
}
|
||||
|
||||
private int readInt() throws IOException {
|
||||
String val = mReader.nextString();
|
||||
return Integer.decode(val);
|
||||
return readValueAsInt((str) -> {
|
||||
throw new IllegalStateException("Encountered malformed data. Expected int.");
|
||||
});
|
||||
}
|
||||
|
||||
private Bus readBus() throws IOException {
|
||||
@@ -338,17 +384,20 @@ public class Event {
|
||||
try {
|
||||
mReader.beginArray();
|
||||
while (mReader.hasNext()) {
|
||||
int type = 0;
|
||||
UinputControlCode controlCode = null;
|
||||
IntStream data = null;
|
||||
mReader.beginObject();
|
||||
while (mReader.hasNext()) {
|
||||
String name = mReader.nextName();
|
||||
switch (name) {
|
||||
case "type":
|
||||
type = readInt();
|
||||
controlCode = readUinputControlCode();
|
||||
break;
|
||||
case "data":
|
||||
data = readIntList().stream().mapToInt(Integer::intValue);
|
||||
Objects.requireNonNull(controlCode,
|
||||
"Configuration 'type' must be specified before 'data'.");
|
||||
data = readDataForControlCode(controlCode)
|
||||
.stream().mapToInt(Integer::intValue);
|
||||
break;
|
||||
default:
|
||||
consumeRemainingElements();
|
||||
@@ -358,9 +407,9 @@ public class Event {
|
||||
}
|
||||
}
|
||||
mReader.endObject();
|
||||
if (data != null) {
|
||||
final int[] existing = configuration.get(type);
|
||||
configuration.put(type, existing == null ? data.toArray()
|
||||
if (controlCode != null && data != null) {
|
||||
final int[] existing = configuration.get(controlCode.mValue);
|
||||
configuration.put(controlCode.mValue, existing == null ? data.toArray()
|
||||
: IntStream.concat(IntStream.of(existing), data).toArray());
|
||||
}
|
||||
}
|
||||
@@ -373,6 +422,60 @@ public class Event {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private UinputControlCode readUinputControlCode() throws IOException {
|
||||
var code = readValueAsInt((controlTypeStr) -> {
|
||||
for (UinputControlCode controlCode : UinputControlCode.values()) {
|
||||
if (controlCode.mName.equals(controlTypeStr)) {
|
||||
return controlCode.mValue;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
for (UinputControlCode controlCode : UinputControlCode.values()) {
|
||||
if (controlCode.mValue == code) {
|
||||
return controlCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Integer> readDataForControlCode(
|
||||
UinputControlCode controlCode) throws IOException {
|
||||
return switch (controlCode) {
|
||||
case UI_SET_EVBIT -> readArrayAsInts(this::readEvdevEventType);
|
||||
case UI_SET_KEYBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_KEY));
|
||||
case UI_SET_RELBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_REL));
|
||||
case UI_SET_ABSBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_ABS));
|
||||
case UI_SET_MSCBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_MSC));
|
||||
case UI_SET_LEDBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_LED));
|
||||
case UI_SET_SNDBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_SND));
|
||||
case UI_SET_FFBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_FF));
|
||||
case UI_SET_SWBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_SW));
|
||||
case UI_SET_PROPBIT -> readArrayAsInts(this::readEvdevInputProp);
|
||||
};
|
||||
}
|
||||
|
||||
interface IntValueReader {
|
||||
int readNextValue() throws IOException;
|
||||
}
|
||||
|
||||
private ArrayList<Integer> readArrayAsInts(
|
||||
IntValueReader nextValueReader) throws IOException {
|
||||
ArrayList<Integer> data = new ArrayList<>();
|
||||
try {
|
||||
mReader.beginArray();
|
||||
while (mReader.hasNext()) {
|
||||
data.add(nextValueReader.readNextValue());
|
||||
}
|
||||
mReader.endArray();
|
||||
} catch (IllegalStateException | NumberFormatException e) {
|
||||
consumeRemainingElements();
|
||||
mReader.endArray();
|
||||
throw new IllegalStateException("Encountered malformed data.", e);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private InputAbsInfo readAbsInfo() throws IllegalStateException, IOException {
|
||||
InputAbsInfo absInfo = new InputAbsInfo();
|
||||
try {
|
||||
@@ -426,7 +529,7 @@ public class Event {
|
||||
String name = mReader.nextName();
|
||||
switch (name) {
|
||||
case "code":
|
||||
type = readInt();
|
||||
type = readEvdevEventCode(EV_ABS);
|
||||
break;
|
||||
case "info":
|
||||
absInfo = readAbsInfo();
|
||||
@@ -452,6 +555,18 @@ public class Event {
|
||||
return infoArray;
|
||||
}
|
||||
|
||||
private int readEvdevEventType() throws IOException {
|
||||
return readValueAsInt(Device::getEvdevEventTypeByLabel);
|
||||
}
|
||||
|
||||
private int readEvdevEventCode(int type) throws IOException {
|
||||
return readValueAsInt((str) -> Device.getEvdevEventCodeByLabel(type, str));
|
||||
}
|
||||
|
||||
private int readEvdevInputProp() throws IOException {
|
||||
return readValueAsInt(Device::getEvdevInputPropByLabel);
|
||||
}
|
||||
|
||||
private void consumeRemainingElements() throws IOException {
|
||||
while (mReader.hasNext()) {
|
||||
mReader.skipValue();
|
||||
|
||||
Reference in New Issue
Block a user