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>
uhid.38
Simon Chan 5 months ago committed by Romain Vimont
parent 4d5b67cc80
commit 840680f546

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

@ -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]'

@ -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',

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save