diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index b2009c56..904ccdeb 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -116,7 +116,7 @@ _scrcpy() { return ;; --keyboard) - COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) + COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) return ;; --mouse) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index a4611632..f81d2b22 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -34,7 +34,7 @@ arguments=( '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' - '--keyboard[Set the keyboard input mode]:mode:(disabled sdk aoa)' + '--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-camera-sizes[List the valid camera capture sizes]' diff --git a/app/meson.build b/app/meson.build index 3ec9781a..9a2d2838 100644 --- a/app/meson.build +++ b/app/meson.build @@ -35,6 +35,7 @@ src = [ 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', + 'src/uhid/keyboard_uhid.c', 'src/util/acksync.c', 'src/util/audiobuf.c', 'src/util/average.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index ed2e620e..1dfcab2b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -175,13 +175,14 @@ Print this help. .BI "\-\-keyboard " mode Select how to send keyboard inputs to the device. -Possible values are "disabled", "sdk" and "aoa": +Possible values are "disabled", "sdk", "uhid" and "aoa": - "disabled" does not send keyboard inputs to the device. - "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. + - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. + - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. -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 "uhid" and "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: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS diff --git a/app/src/cli.c b/app/src/cli.c index 364590a4..59cd5699 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -365,19 +365,22 @@ static const struct sc_option options[] = { .longopt = "keyboard", .argdesc = "mode", .text = "Select how to send keyboard inputs to the device.\n" - "Possible values are \"disabled\", \"sdk\" and \"aoa\".\n" + "Possible values are \"disabled\", \"sdk\", \"uhid\" and " + "\"aoa\".\n" "\"disabled\" does not send keyboard inputs to the device.\n" "\"sdk\" uses the Android system API to deliver keyboard " "events to applications.\n" + "\"uhid\" simulates a physical HID keyboard using the Linux " + "UHID kernel module on the device.\n" "\"aoa\" simulates a physical keyboard using the AOAv2 " "protocol. It may only work over USB.\n" - "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: `adb shell am start -a " + "For \"uhid\" and \"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: `adb shell am start -a " "android.settings.HARD_KEYBOARD_SETTINGS`.\n" - "This option is only available when the HID keyboard is " - "enabled (or a physical keyboard is connected).\n" + "This option is only available when a HID keyboard is enabled " + "(or a physical keyboard is connected).\n" "Also see --mouse.", }, { @@ -1939,6 +1942,11 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { return true; } + if (!strcmp(optarg, "uhid")) { + *mode = SC_KEYBOARD_INPUT_MODE_UHID; + return true; + } + if (!strcmp(optarg, "aoa")) { #ifdef HAVE_USB *mode = SC_KEYBOARD_INPUT_MODE_AOA; @@ -1949,7 +1957,8 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { #endif } - LOGE("Unsupported keyboard: %s (expected disabled, sdk or aoa)", optarg); + LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)", + optarg); return false; } diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e173dac7..88575b4e 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -146,6 +146,17 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; + case SC_CONTROL_MSG_TYPE_UHID_CREATE: + sc_write16be(&buf[1], msg->uhid_create.id); + sc_write16be(&buf[3], msg->uhid_create.report_desc_size); + memcpy(&buf[5], msg->uhid_create.report_desc, + msg->uhid_create.report_desc_size); + return 5 + msg->uhid_create.report_desc_size; + case SC_CONTROL_MSG_TYPE_UHID_INPUT: + sc_write16be(&buf[1], msg->uhid_input.id); + sc_write16be(&buf[3], msg->uhid_input.size); + memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); + return 5 + msg->uhid_input.size; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -242,6 +253,23 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; + case SC_CONTROL_MSG_TYPE_UHID_CREATE: + LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16, + msg->uhid_create.id, msg->uhid_create.report_desc_size); + break; + case SC_CONTROL_MSG_TYPE_UHID_INPUT: { + char *hex = sc_str_to_hex_string(msg->uhid_input.data, + msg->uhid_input.size); + if (hex) { + LOG_CMSG("UHID input [%" PRIu16 "] %s", + msg->uhid_input.id, hex); + free(hex); + } else { + LOG_CMSG("UHID input [%" PRIu16 "] size=%" PRIu16, + msg->uhid_input.id, msg->uhid_input.size); + } + break; + } default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 04eeb83b..550168c2 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,6 +10,7 @@ #include "android/input.h" #include "android/keycodes.h" #include "coords.h" +#include "hid/hid_event.h" #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_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, + SC_CONTROL_MSG_TYPE_UHID_CREATE, + SC_CONTROL_MSG_TYPE_UHID_INPUT, }; enum sc_screen_power_mode { @@ -92,6 +95,16 @@ struct sc_control_msg { struct { enum sc_screen_power_mode mode; } set_screen_power_mode; + struct { + uint16_t id; + uint16_t report_desc_size; + const uint8_t *report_desc; // pointer to static data + } uhid_create; + struct { + uint16_t id; + uint16_t size; + uint8_t data[SC_HID_MAX_SIZE]; + } uhid_input; }; }; diff --git a/app/src/options.h b/app/src/options.h index 1fb31c1a..6d62fac0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -143,6 +143,7 @@ enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_SDK, + SC_KEYBOARD_INPUT_MODE_UHID, SC_KEYBOARD_INPUT_MODE_AOA, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a407dff1..d01d3619 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -25,6 +25,7 @@ #include "recorder.h" #include "screen.h" #include "server.h" +#include "uhid/keyboard_uhid.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/keyboard_aoa.h" @@ -64,6 +65,7 @@ struct scrcpy { #endif union { struct sc_keyboard_sdk keyboard_sdk; + struct sc_keyboard_uhid keyboard_uhid; #ifdef HAVE_USB struct sc_keyboard_aoa keyboard_aoa; #endif @@ -656,15 +658,22 @@ aoa_hid_end: assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif - // keyboard_input_mode may have been reset if HID mode failed + // keyboard_input_mode may have been reset if AOA mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, options->key_inject_mode, options->forward_key_repeat); 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 AOA mode failed if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); mp = &s->mouse_sdk.mouse_processor; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c new file mode 100644 index 00000000..d974d578 --- /dev/null +++ b/app/src/uhid/keyboard_uhid.c @@ -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; +} diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h new file mode 100644 index 00000000..854ba008 --- /dev/null +++ b/app/src/uhid/keyboard_uhid.h @@ -0,0 +1,23 @@ +#ifndef SC_KEYBOARD_UHID_H +#define SC_KEYBOARD_UHID_H + +#include "common.h" + +#include + +#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 diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 80d33fc3..0ab61153 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -323,6 +323,53 @@ static void test_serialize_rotate_device(void) { 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_size = sizeof(report_desc), + .report_desc = report_desc, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 16); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_CREATE, + 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, + .size = 5, + .data = {1, 2, 3, 4, 5}, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 10); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_INPUT, + 0, 42, // id + 0, 5, // size + 1, 2, 3, 4, 5, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -341,5 +388,7 @@ int main(int argc, char *argv[]) { test_serialize_set_clipboard_long(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); + test_serialize_uhid_create(); + test_serialize_uhid_input(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index e1800374..74bf5610 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -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; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index d95c36d8..24aa73c0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -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 value = 0; + for (int i = 0; i < sizeBytes; ++i) { + value = (value << 8) | (buffer.get() & 0xFF); } - int len = buffer.getInt(); - if (buffer.remaining() < len) { + 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(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index a3508c96..d757d577 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -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 } diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java new file mode 100644 index 00000000..96458bf0 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java @@ -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 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()); + } + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 47097c78..7cc67c3e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -322,6 +322,50 @@ public class ControlMessageReaderTest { 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.writeShort(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.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5}; + 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_INPUT, event.getType()); + Assert.assertEquals(42, event.getId()); + Assert.assertArrayEquals(data, event.getData()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader();