Merge "Uinput shell command: Support string literals for evdev event codes" into main

This commit is contained in:
Prabir Pradhan
2023-07-28 13:26:40 +00:00
committed by Android (Google) Code Review
5 changed files with 237 additions and 66 deletions

View File

@@ -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
]
}
```

View File

@@ -21,6 +21,7 @@ cc_library_shared {
"libbase",
"libbinder",
"liblog",
"libinput",
"libnativehelper",
],

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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();