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 bfe6455183
commit 3fa70d55f1
17 changed files with 513 additions and 20 deletions

View File

@ -116,7 +116,7 @@ _scrcpy() {
return return
;; ;;
--keyboard) --keyboard)
COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$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 uhid aoa)'
'--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

@ -35,6 +35,7 @@ src = [
'src/hid/hid_mouse.c', 'src/hid/hid_mouse.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

@ -175,13 +175,14 @@ Print this help.
.BI "\-\-keyboard " mode .BI "\-\-keyboard " mode
Select how to send keyboard inputs to the device. 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. - "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. - "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 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\", \"uhid\" and "
"\"aoa\".\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" "\"uhid\" simulates a physical HID keyboard using the Linux "
"UHID kernel module on the device."
"\"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 " "For \"uhid\" and \"aoa\", the keyboard layout must be "
"for all) on the device, via Settings -> System -> Languages " "configured (once and for all) on the device, via Settings -> "
"and input -> Physical keyboard. This settings page can be " "System -> Languages and input -> Physical keyboard. This "
"started directly: `adb shell am start -a " "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.",
}, },
@ -1939,6 +1942,11 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) {
return true; return true;
} }
if (!strcmp(optarg, "uhid")) {
*mode = SC_KEYBOARD_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) { if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB #ifdef HAVE_USB
*mode = SC_KEYBOARD_INPUT_MODE_AOA; *mode = SC_KEYBOARD_INPUT_MODE_AOA;
@ -1949,7 +1957,8 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) {
#endif #endif
} }
LOGE("Unsupported keyboard: %s (expected disabled, sdk or aoa)", optarg); LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)",
optarg);
return false; return false;
} }

View File

@ -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: case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;
return 2; 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_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: 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: 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 [%" 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: 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 {
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;
}; };
}; };

View File

@ -143,6 +143,7 @@ enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_DISABLED,
SC_KEYBOARD_INPUT_MODE_SDK, SC_KEYBOARD_INPUT_MODE_SDK,
SC_KEYBOARD_INPUT_MODE_UHID,
SC_KEYBOARD_INPUT_MODE_AOA, SC_KEYBOARD_INPUT_MODE_AOA,
}; };

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"
@ -64,6 +65,7 @@ struct scrcpy {
#endif #endif
union { union {
struct sc_keyboard_sdk keyboard_sdk; struct sc_keyboard_sdk keyboard_sdk;
struct sc_keyboard_uhid keyboard_uhid;
#ifdef HAVE_USB #ifdef HAVE_USB
struct sc_keyboard_aoa keyboard_aoa; struct sc_keyboard_aoa keyboard_aoa;
#endif #endif
@ -656,15 +658,22 @@ aoa_hid_end:
assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA);
#endif #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) { if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) {
sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller,
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 AOA mode failed
if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); sc_mouse_sdk_init(&s->mouse_sdk, &s->controller);
mp = &s->mouse_sdk.mouse_processor; mp = &s->mouse_sdk.mouse_processor;

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_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[]) { 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 = 4;
static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4;
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.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) { 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() {
@ -96,6 +99,7 @@ public class Controller implements AsyncProcessor {
Ln.e("Controller error", e); Ln.e("Controller error", e);
} finally { } finally {
Ln.d("Controller stopped"); Ln.d("Controller stopped");
uhidManager.closeAll();
listener.onTerminated(true); listener.onTerminated(true);
} }
}, "control-recv"); }, "control-recv");
@ -190,6 +194,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:
uhidOpen(msg.getId(), msg.getData());
break;
case ControlMessage.TYPE_UHID_INPUT:
uhidWriteInput(msg.getId(), msg.getData());
break;
default: default:
// do nothing // do nothing
} }
@ -426,4 +436,20 @@ public class Controller implements AsyncProcessor {
return ok; return ok;
} }
private void uhidOpen(int id, byte[] data) {
try {
uhidManager.open(id, data);
} catch (IOException e) {
Ln.e("Could not open UHID", e);
}
}
private void uhidWriteInput(int id, byte[] data) {
try {
uhidManager.writeInput(id, data);
} catch (IOException e) {
Ln.e("Could not write UHID input", e);
}
}
} }

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

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.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 @Test
public void testMultiEvents() throws IOException { public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader(); ControlMessageReader reader = new ControlMessageReader();