Add UHID keyboard support

Use the following command:

    scrcpy --keyboard=uhid

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
Simon Chan 2023-11-28 17:17:35 +08:00 committed by Romain Vimont
parent cbd3289db0
commit adb69e1870
17 changed files with 483 additions and 17 deletions

View file

@ -117,7 +117,7 @@ _scrcpy() {
return return
;; ;;
--keyboard) --keyboard)
COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) COMPREPLY=($(compgen -W 'disabled sdk aoa uhid' -- "$cur"))
return return
;; ;;
--mouse) --mouse)

View file

@ -34,7 +34,7 @@ arguments=(
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to device]' '--forward-all-clicks[Forward clicks to device]'
{-h,--help}'[Print the help]' {-h,--help}'[Print the help]'
'--keyboard[Set the keyboard input mode]:mode:(disabled sdk aoa)' '--keyboard[Set the keyboard input mode]:mode:(disabled sdk aoa uhid)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-camera-sizes[List the valid camera capture sizes]' '--list-camera-sizes[List the valid camera capture sizes]'

View file

@ -33,6 +33,7 @@ src = [
'src/version.c', 'src/version.c',
'src/trait/frame_source.c', 'src/trait/frame_source.c',
'src/trait/packet_source.c', 'src/trait/packet_source.c',
'src/uhid/keyboard_uhid.c',
'src/util/acksync.c', 'src/util/acksync.c',
'src/util/audiobuf.c', 'src/util/audiobuf.c',
'src/util/average.c', 'src/util/average.c',

View file

@ -179,9 +179,10 @@ Possible values are "disabled", "sdk" and "aoa":
- "disabled" does not send keyboard inputs to the device. - "disabled" does not send keyboard inputs to the device.
- "sdk" uses the Android system API to deliver keyboard events to applications. - "sdk" uses the Android system API to deliver keyboard events to applications.
- "aoa" simulates a physical keyboard using the AOAv2 protocol. It may only work over USB. - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB.
- "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device.
For "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: For "aoa" and "uhid", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly:
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS

View file

@ -365,18 +365,21 @@ static const struct sc_option options[] = {
.longopt = "keyboard", .longopt = "keyboard",
.argdesc = "mode", .argdesc = "mode",
.text = "Select how to send keyboard inputs to the device.\n" .text = "Select how to send keyboard inputs to the device.\n"
"Possible values are \"disabled\", \"sdk\" and \"aoa\".\n" "Possible values are \"disabled\", \"sdk\", \"aoa\" and "
"\"uhid\".\n"
"\"disabled\" does not send keyboard inputs to the device.\n" "\"disabled\" does not send keyboard inputs to the device.\n"
"\"sdk\" uses the Android system API to deliver keyboard\n" "\"sdk\" uses the Android system API to deliver keyboard\n"
"events to applications.\n" "events to applications.\n"
"\"aoa\" simulates a physical keyboard using the AOAv2\n" "\"aoa\" simulates a physical HID keyboard using the AOAv2\n"
"protocol. It may only work over USB.\n" "protocol. It may only work over USB.\n"
"For \"aoa\", the keyboard layout must be configured (once and " "\"uhid\" simulates a physical HID keyboard using the Linux "
"for all) on the device, via Settings -> System -> Languages " "UHID kernel module on the device."
"and input -> Physical keyboard. This settings page can be " "For \"aoa\" and \"uhid\", the keyboard layout must be "
"started directly: `adb shell am start -a " "configured (once and for all) on the device, via Settings -> "
"System -> Languages and input -> Physical keyboard. This "
"settings page can be started directly: `adb shell am start -a "
"android.settings.HARD_KEYBOARD_SETTINGS`.\n" "android.settings.HARD_KEYBOARD_SETTINGS`.\n"
"This option is only available when the HID keyboard is " "This option is only available when a HID keyboard is enabled "
"enabled (or a physical keyboard is connected).\n" "enabled (or a physical keyboard is connected).\n"
"Also see --mouse.", "Also see --mouse.",
}, },
@ -1949,7 +1952,13 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) {
#endif #endif
} }
LOGE("Unsupported keyboard: %s (expected disabled, sdk or aoa)", optarg); if (!strcmp(optarg, "uhid")) {
*mode = SC_KEYBOARD_INPUT_MODE_UHID;
return true;
}
LOGE("Unsupported keyboard: %s (expected disabled, sdk, aoa or uhid)",
optarg);
return false; return false;
} }

View file

@ -152,6 +152,17 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data // no additional data
return 1; return 1;
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
sc_write32be(&buf[1], msg->uhid_create.id);
sc_write16be(&buf[5], msg->uhid_create.report_desc_size);
memcpy(&buf[7], msg->uhid_create.report_desc,
msg->uhid_create.report_desc_size);
return 7 + msg->uhid_create.report_desc_size;
case SC_CONTROL_MSG_TYPE_UHID_INPUT:
sc_write32be(&buf[1], msg->uhid_input.id);
buf[5] = msg->uhid_input.size;
memcpy(&buf[6], msg->uhid_input.data, msg->uhid_input.size);
return 6 + msg->uhid_input.size;
default: default:
LOGW("Unknown message type: %u", (unsigned) msg->type); LOGW("Unknown message type: %u", (unsigned) msg->type);
return 0; return 0;
@ -242,6 +253,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
LOG_CMSG("rotate device"); LOG_CMSG("rotate device");
break; break;
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
LOG_CMSG("UHID create (report_desc size=%" PRIu16 ")",
msg->uhid_create.report_desc_size);
break;
case SC_CONTROL_MSG_TYPE_UHID_INPUT:
LOG_CMSG("UHID input (size=%" PRIu8 ")", msg->uhid_input.size);
break;
default: default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type); LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break; break;

View file

@ -10,6 +10,7 @@
#include "android/input.h" #include "android/input.h"
#include "android/keycodes.h" #include "android/keycodes.h"
#include "coords.h" #include "coords.h"
#include "hid/hid_event.h"
#define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
@ -37,6 +38,8 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT,
}; };
enum sc_screen_power_mode { enum sc_screen_power_mode {
@ -92,6 +95,16 @@ struct sc_control_msg {
struct { struct {
enum sc_screen_power_mode mode; enum sc_screen_power_mode mode;
} set_screen_power_mode; } set_screen_power_mode;
struct {
uint32_t id;
const uint8_t *report_desc; // pointer to static data
uint16_t report_desc_size;
} uhid_create;
struct {
uint32_t id;
uint8_t data[SC_HID_MAX_SIZE];
uint8_t size;
} uhid_input;
}; };
}; };

View file

@ -144,6 +144,7 @@ enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_DISABLED,
SC_KEYBOARD_INPUT_MODE_SDK, SC_KEYBOARD_INPUT_MODE_SDK,
SC_KEYBOARD_INPUT_MODE_AOA, SC_KEYBOARD_INPUT_MODE_AOA,
SC_KEYBOARD_INPUT_MODE_UHID,
}; };
enum sc_mouse_input_mode { enum sc_mouse_input_mode {

View file

@ -25,6 +25,7 @@
#include "recorder.h" #include "recorder.h"
#include "screen.h" #include "screen.h"
#include "server.h" #include "server.h"
#include "uhid/keyboard_uhid.h"
#ifdef HAVE_USB #ifdef HAVE_USB
# include "usb/aoa_hid.h" # include "usb/aoa_hid.h"
# include "usb/keyboard_aoa.h" # include "usb/keyboard_aoa.h"
@ -67,6 +68,7 @@ struct scrcpy {
#ifdef HAVE_USB #ifdef HAVE_USB
struct sc_keyboard_aoa keyboard_aoa; struct sc_keyboard_aoa keyboard_aoa;
#endif #endif
struct sc_keyboard_uhid keyboard_uhid;
}; };
union { union {
struct sc_mouse_sdk mouse_sdk; struct sc_mouse_sdk mouse_sdk;
@ -667,6 +669,12 @@ aoa_hid_end:
options->key_inject_mode, options->key_inject_mode,
options->forward_key_repeat); options->forward_key_repeat);
kp = &s->keyboard_sdk.key_processor; kp = &s->keyboard_sdk.key_processor;
} else if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_UHID) {
bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller);
if (!ok) {
goto end;
}
kp = &s->keyboard_uhid.key_processor;
} }
// mouse_input_mode may have been reset if HID mode failed // mouse_input_mode may have been reset if HID mode failed

View file

@ -0,0 +1,72 @@
#include "keyboard_uhid.h"
#include "util/log.h"
/** Downcast key processor to keyboard_uhid */
#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor)
#define UHID_KEYBOARD_ID 1
static void
sc_key_processor_process_key(struct sc_key_processor *kp,
const struct sc_key_event *event,
uint64_t ack_to_wait) {
(void) ack_to_wait;
if (event->repeat) {
// In USB HID protocol, key repeat is handled by the host (Android), so
// just ignore key repeat here.
return;
}
struct sc_keyboard_uhid *kb = DOWNCAST(kp);
struct sc_hid_event hid_event;
// Not all keys are supported, just ignore unsupported keys
if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = UHID_KEYBOARD_ID;
assert(hid_event.size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, hid_event.data, hid_event.size);
msg.uhid_input.size = hid_event.size;
if (!sc_controller_push_msg(kb->controller, &msg)) {
LOGE("Could not send UHID_INPUT message (key)");
}
}
}
bool
sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_controller *controller) {
sc_hid_keyboard_init(&kb->hid);
kb->controller = controller;
static const struct sc_key_processor_ops ops = {
.process_key = sc_key_processor_process_key,
// Never forward text input via HID (all the keys are injected
// separately)
.process_text = NULL,
};
// Clipboard synchronization is requested over the same control socket, so
// there is no need for a specific synchronization mechanism
kb->key_processor.async_paste = false;
kb->key_processor.ops = &ops;
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = UHID_KEYBOARD_ID;
msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC;
msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN;
if (!sc_controller_push_msg(controller, &msg)) {
LOGE("Could not send UHID_CREATE message (keyboard)");
return false;
}
return true;
}

View file

@ -0,0 +1,23 @@
#ifndef SC_KEYBOARD_UHID_H
#define SC_KEYBOARD_UHID_H
#include "common.h"
#include <stdbool.h>
#include "controller.h"
#include "hid/hid_keyboard.h"
#include "trait/key_processor.h"
struct sc_keyboard_uhid {
struct sc_key_processor key_processor; // key processor trait
struct sc_hid_keyboard hid;
struct sc_controller *controller;
};
bool
sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_controller *controller);
#endif

View file

@ -323,6 +323,53 @@ static void test_serialize_rotate_device(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_uhid_create(void) {
const uint8_t report_desc[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
.uhid_create = {
.id = 42,
.report_desc = report_desc,
.report_desc_size = sizeof(report_desc),
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 18);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_CREATE,
0, 0, 0, 42, // id
0, 11, // size
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_uhid_input(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_UHID_INPUT,
.uhid_input = {
.id = 42,
.data = {1, 2, 3, 4, 5},
.size = 5,
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 11);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_INPUT,
0, 0, 0, 42, // id
5, // size
1, 2, 3, 4, 5,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -341,5 +388,7 @@ int main(int argc, char *argv[]) {
test_serialize_set_clipboard_long(); test_serialize_set_clipboard_long();
test_serialize_set_screen_power_mode(); test_serialize_set_screen_power_mode();
test_serialize_rotate_device(); test_serialize_rotate_device();
test_serialize_uhid_create();
test_serialize_uhid_input();
return 0; return 0;
} }

View file

@ -17,6 +17,8 @@ public final class ControlMessage {
public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11; 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; public static final long SEQUENCE_INVALID = 0;
@ -40,6 +42,8 @@ public final class ControlMessage {
private boolean paste; private boolean paste;
private int repeat; private int repeat;
private long sequence; private long sequence;
private int id;
private byte[] data;
private ControlMessage() { private ControlMessage() {
} }
@ -123,6 +127,22 @@ public final class ControlMessage {
return msg; 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() { public int getType() {
return type; return type;
} }
@ -186,4 +206,12 @@ public final class ControlMessage {
public long getSequence() { public long getSequence() {
return sequence; return sequence;
} }
public int getId() {
return id;
}
public byte[] getData() {
return data;
}
} }

View file

@ -15,6 +15,8 @@ public class ControlMessageReader {
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int GET_CLIPBOARD_LENGTH = 1; static final int GET_CLIPBOARD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 6;
static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 5;
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
@ -86,6 +88,12 @@ public class ControlMessageReader {
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type); msg = ControlMessage.createEmpty(type);
break; break;
case ControlMessage.TYPE_UHID_CREATE:
msg = parseUhidCreate();
break;
case ControlMessage.TYPE_UHID_INPUT:
msg = parseUhidInput();
break;
default: default:
Ln.w("Unknown event type: " + type); Ln.w("Unknown event type: " + type);
msg = null; msg = null;
@ -110,12 +118,21 @@ public class ControlMessageReader {
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
} }
private String parseString() { private int parseBufferLength(int sizeBytes) {
if (buffer.remaining() < 4) { assert sizeBytes > 0 && sizeBytes <= 4;
return null; if (buffer.remaining() < sizeBytes) {
return -1;
} }
int len = buffer.getInt(); int value = 0;
if (buffer.remaining() < len) { 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; return null;
} }
int position = buffer.position(); int position = buffer.position();
@ -124,6 +141,16 @@ public class ControlMessageReader {
return new String(rawBuffer, position, len, StandardCharsets.UTF_8); 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() { private ControlMessage parseInjectText() {
String text = parseString(); String text = parseString();
if (text == null) { if (text == null) {
@ -193,6 +220,30 @@ public class ControlMessageReader {
return ControlMessage.createSetScreenPowerMode(mode); return ControlMessage.createSetScreenPowerMode(mode);
} }
private ControlMessage parseUhidCreate() {
if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) {
return null;
}
int id = buffer.getInt();
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.getInt();
byte[] data = parseByteArray(1);
if (data == null) {
return null;
}
return ControlMessage.createUhidInput(id, data);
}
private static Position readPosition(ByteBuffer buffer) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = buffer.getInt();
int y = buffer.getInt(); int y = buffer.getInt();

View file

@ -26,6 +26,8 @@ public class Controller implements AsyncProcessor {
private Thread thread; private Thread thread;
private final UhidManager uhidManager;
private final Device device; private final Device device;
private final ControlChannel controlChannel; private final ControlChannel controlChannel;
private final CleanUp cleanUp; private final CleanUp cleanUp;
@ -50,6 +52,7 @@ public class Controller implements AsyncProcessor {
this.powerOn = powerOn; this.powerOn = powerOn;
initPointers(); initPointers();
sender = new DeviceMessageSender(controlChannel); sender = new DeviceMessageSender(controlChannel);
uhidManager = new UhidManager();
} }
private void initPointers() { private void initPointers() {
@ -95,6 +98,7 @@ public class Controller implements AsyncProcessor {
// this is expected on close // this is expected on close
} finally { } finally {
Ln.d("Controller stopped"); Ln.d("Controller stopped");
uhidManager.closeAll();
listener.onTerminated(true); listener.onTerminated(true);
} }
}, "control-recv"); }, "control-recv");
@ -182,6 +186,12 @@ public class Controller implements AsyncProcessor {
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(); Device.rotateDevice();
break; break;
case ControlMessage.TYPE_UHID_CREATE:
uhidManager.open(msg.getId(), msg.getData());
break;
case ControlMessage.TYPE_UHID_INPUT:
uhidManager.write(msg.getId(), msg.getData());
break;
default: default:
// do nothing // do nothing
} }

View 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 write(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());
}
}
}

View file

@ -322,6 +322,50 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType());
} }
@Test
public void testParseUhidCreate() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_UHID_CREATE);
dos.writeInt(42); // id
byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
dos.writeShort(data.length); // size
dos.write(data);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType());
Assert.assertEquals(42, event.getId());
Assert.assertArrayEquals(data, event.getData());
}
@Test
public void testParseUhidInput() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_UHID_INPUT);
dos.writeInt(42); // id
byte[] data = {1, 2, 3, 4, 5};
dos.writeByte(data.length); // size
dos.write(data);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType());
Assert.assertEquals(42, event.getId());
Assert.assertArrayEquals(data, event.getData());
}
@Test @Test
public void testMultiEvents() throws IOException { public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader(); ControlMessageReader reader = new ControlMessageReader();