mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-01-25 16:44:26 +01:00
Add UHID keyboard support
Use the following command:
scrcpy --keyboard=uhid
PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
committed by
Romain Vimont
parent
4d5b67cc80
commit
840680f546
@@ -17,6 +17,8 @@ public final class ControlMessage {
|
||||
public static final int TYPE_SET_CLIPBOARD = 9;
|
||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
|
||||
public static final int TYPE_ROTATE_DEVICE = 11;
|
||||
public static final int TYPE_UHID_CREATE = 12;
|
||||
public static final int TYPE_UHID_INPUT = 13;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
@@ -40,6 +42,8 @@ public final class ControlMessage {
|
||||
private boolean paste;
|
||||
private int repeat;
|
||||
private long sequence;
|
||||
private int id;
|
||||
private byte[] data;
|
||||
|
||||
private ControlMessage() {
|
||||
}
|
||||
@@ -123,6 +127,22 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createUhidCreate(int id, byte[] reportDesc) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_UHID_CREATE;
|
||||
msg.id = id;
|
||||
msg.data = reportDesc;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createUhidInput(int id, byte[] data) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_UHID_INPUT;
|
||||
msg.id = id;
|
||||
msg.data = data;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
@@ -186,4 +206,12 @@ public final class ControlMessage {
|
||||
public long getSequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ public class ControlMessageReader {
|
||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
static final int GET_CLIPBOARD_LENGTH = 1;
|
||||
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
|
||||
static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4;
|
||||
static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4;
|
||||
|
||||
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||
|
||||
@@ -86,6 +88,12 @@ public class ControlMessageReader {
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
msg = ControlMessage.createEmpty(type);
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
msg = parseUhidCreate();
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
msg = parseUhidInput();
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown event type: " + type);
|
||||
msg = null;
|
||||
@@ -110,12 +118,21 @@ public class ControlMessageReader {
|
||||
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
if (buffer.remaining() < 4) {
|
||||
return null;
|
||||
private int parseBufferLength(int sizeBytes) {
|
||||
assert sizeBytes > 0 && sizeBytes <= 4;
|
||||
if (buffer.remaining() < sizeBytes) {
|
||||
return -1;
|
||||
}
|
||||
int len = buffer.getInt();
|
||||
if (buffer.remaining() < len) {
|
||||
int value = 0;
|
||||
for (int i = 0; i < sizeBytes; ++i) {
|
||||
value = (value << 8) | (buffer.get() & 0xFF);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
int len = parseBufferLength(4);
|
||||
if (len == -1 || buffer.remaining() < len) {
|
||||
return null;
|
||||
}
|
||||
int position = buffer.position();
|
||||
@@ -124,6 +141,16 @@ public class ControlMessageReader {
|
||||
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private byte[] parseByteArray(int sizeBytes) {
|
||||
int len = parseBufferLength(sizeBytes);
|
||||
if (len == -1 || buffer.remaining() < len) {
|
||||
return null;
|
||||
}
|
||||
byte[] data = new byte[len];
|
||||
buffer.get(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectText() {
|
||||
String text = parseString();
|
||||
if (text == null) {
|
||||
@@ -193,6 +220,30 @@ public class ControlMessageReader {
|
||||
return ControlMessage.createSetScreenPowerMode(mode);
|
||||
}
|
||||
|
||||
private ControlMessage parseUhidCreate() {
|
||||
if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int id = buffer.getShort();
|
||||
byte[] data = parseByteArray(2);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return ControlMessage.createUhidCreate(id, data);
|
||||
}
|
||||
|
||||
private ControlMessage parseUhidInput() {
|
||||
if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int id = buffer.getShort();
|
||||
byte[] data = parseByteArray(2);
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return ControlMessage.createUhidInput(id, data);
|
||||
}
|
||||
|
||||
private static Position readPosition(ByteBuffer buffer) {
|
||||
int x = buffer.getInt();
|
||||
int y = buffer.getInt();
|
||||
|
||||
@@ -26,6 +26,8 @@ public class Controller implements AsyncProcessor {
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private final UhidManager uhidManager;
|
||||
|
||||
private final Device device;
|
||||
private final ControlChannel controlChannel;
|
||||
private final CleanUp cleanUp;
|
||||
@@ -50,6 +52,7 @@ public class Controller implements AsyncProcessor {
|
||||
this.powerOn = powerOn;
|
||||
initPointers();
|
||||
sender = new DeviceMessageSender(controlChannel);
|
||||
uhidManager = new UhidManager();
|
||||
}
|
||||
|
||||
private void initPointers() {
|
||||
@@ -96,6 +99,7 @@ public class Controller implements AsyncProcessor {
|
||||
Ln.e("Controller error", e);
|
||||
} finally {
|
||||
Ln.d("Controller stopped");
|
||||
uhidManager.closeAll();
|
||||
listener.onTerminated(true);
|
||||
}
|
||||
}, "control-recv");
|
||||
@@ -190,6 +194,12 @@ public class Controller implements AsyncProcessor {
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
device.rotateDevice();
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
uhidManager.open(msg.getId(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
uhidManager.writeInput(msg.getId(), msg.getData());
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
138
server/src/main/java/com/genymobile/scrcpy/UhidManager.java
Normal file
138
server/src/main/java/com/genymobile/scrcpy/UhidManager.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class UhidManager {
|
||||
|
||||
// Linux: include/uapi/linux/uhid.h
|
||||
private static final int UHID_CREATE2 = 11;
|
||||
private static final int UHID_INPUT2 = 12;
|
||||
|
||||
// Linux: include/uapi/linux/input.h
|
||||
private static final short BUS_VIRTUAL = 0x06;
|
||||
|
||||
private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>();
|
||||
|
||||
public void open(int id, byte[] reportDesc) throws IOException {
|
||||
try {
|
||||
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
|
||||
try {
|
||||
FileDescriptor old = fds.put(id, fd);
|
||||
if (old != null) {
|
||||
Ln.w("Duplicate UHID id: " + id);
|
||||
close(old);
|
||||
}
|
||||
|
||||
byte[] req = buildUhidCreate2Req(reportDesc);
|
||||
Os.write(fd, req, 0, req.length);
|
||||
} catch (Exception e) {
|
||||
close(fd);
|
||||
throw e;
|
||||
}
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeInput(int id, byte[] data) throws IOException {
|
||||
FileDescriptor fd = fds.get(id);
|
||||
if (fd == null) {
|
||||
Ln.w("Unknown UHID id: " + id);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] req = buildUhidInput2Req(data);
|
||||
Os.write(fd, req, 0, req.length);
|
||||
} catch (ErrnoException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] buildUhidCreate2Req(byte[] reportDesc) {
|
||||
/*
|
||||
* struct uhid_event {
|
||||
* uint32_t type;
|
||||
* union {
|
||||
* // ...
|
||||
* struct uhid_create2_req {
|
||||
* uint8_t name[128];
|
||||
* uint8_t phys[64];
|
||||
* uint8_t uniq[64];
|
||||
* uint16_t rd_size;
|
||||
* uint16_t bus;
|
||||
* uint32_t vendor;
|
||||
* uint32_t product;
|
||||
* uint32_t version;
|
||||
* uint32_t country;
|
||||
* uint8_t rd_data[HID_MAX_DESCRIPTOR_SIZE];
|
||||
* };
|
||||
* };
|
||||
* } __attribute__((__packed__));
|
||||
*/
|
||||
|
||||
byte[] empty = new byte[256];
|
||||
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
|
||||
buf.putInt(UHID_CREATE2);
|
||||
buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII));
|
||||
buf.put(empty, 0, 256 - "scrcpy".length());
|
||||
buf.putShort((short) reportDesc.length);
|
||||
buf.putShort(BUS_VIRTUAL);
|
||||
buf.putInt(0); // vendor id
|
||||
buf.putInt(0); // product id
|
||||
buf.putInt(0); // version
|
||||
buf.putInt(0); // country;
|
||||
buf.put(reportDesc);
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
private static byte[] buildUhidInput2Req(byte[] data) {
|
||||
/*
|
||||
* struct uhid_event {
|
||||
* uint32_t type;
|
||||
* union {
|
||||
* // ...
|
||||
* struct uhid_input2_req {
|
||||
* uint16_t size;
|
||||
* uint8_t data[UHID_DATA_MAX];
|
||||
* };
|
||||
* };
|
||||
* } __attribute__((__packed__));
|
||||
*/
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(6 + data.length).order(ByteOrder.nativeOrder());
|
||||
buf.putInt(UHID_INPUT2);
|
||||
buf.putShort((short) data.length);
|
||||
buf.put(data);
|
||||
return buf.array();
|
||||
}
|
||||
|
||||
public void close(int id) {
|
||||
FileDescriptor fd = fds.get(id);
|
||||
assert fd != null;
|
||||
close(fd);
|
||||
}
|
||||
|
||||
public void closeAll() {
|
||||
for (FileDescriptor fd : fds.values()) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
private static void close(FileDescriptor fd) {
|
||||
try {
|
||||
Os.close(fd);
|
||||
} catch (ErrnoException e) {
|
||||
Ln.e("Failed to close uhid: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user