Compare commits

..

40 Commits

Author SHA1 Message Date
Romain Vimont
b587014ccf Add UHID gamepad rumble WIP 2024-09-09 08:38:45 +02:00
Romain Vimont
890ee4d893 Simplify UHID outputs routing
There was a registration mechanism to listen to HID outputs with a
specific HID id.

However, the UHID gamepad processor handles several ids, so it will not
work. We could complexify the registration mechanism, but instead,
directly dispatch to the expected processor based on the UHID id.

Concretely, instead of passing a sc_uhid_devices instance to construct a
sc_keyboard_uhid, so that it can register itself, construct the
sc_uhid_devices with all the UHID instances (currently only
sc_keyboard_uhid) so that it can dispatch HID outputs directly.
2024-09-09 08:38:45 +02:00
Romain Vimont
2e79f4b9ef Expose custom UHID device name
Initialize UHID devices with a custom name, starting with a "scrcpy: "
prefix, followed by:
 - "Keyboard" for the keyboard;
 - "Mouse" for the mouse;
 - the actual gamepad name (as reported by SDL) for gamepads.

This is especially convenient for gamepads.
2024-09-09 08:38:45 +02:00
Romain Vimont
676a83610d Reorder function parameters for consistency
Make the local function write_string() accept the output buffer as a
first parameter, like the other similar functions.
2024-09-09 08:38:45 +02:00
Romain Vimont
f151992e08 Make -K -M and -G use AOA in OTG mode
For convenience, short options were added to select UHID input modes:
 - -K for --keyboard=uhid
 - -M for --mouse=uhid
 - -G for --gamepad=uhid

In OTG mode, UHID is not available, so the short options should select
AOA instead.
2024-09-09 08:38:45 +02:00
Romain Vimont
54b6fa5f6e Add UHID gamepad support
Similar to UHID keyboard and mouse, but for gamepads.

Can be enabled with --gamepad=uhid or -G.

It is not enabled by default because not all devices support UHID
(there is a permission error on old Android versions).
2024-09-09 08:38:45 +02:00
Romain Vimont
2d32cbc716 Add UHID_DESTROY control message
This message will be sent on gamepad disconnection.

Contrary to keyboard and mouse, which are registered once and are
unregistered when scrcpy exists, each gamepad is mapped with its own HID
id, and they can be plugged/unplugged dynamically.
2024-09-09 08:38:45 +02:00
Romain Vimont
29105b240b Add gamepad support in OTG mode
Implement gamepad support for OTG.
2024-09-09 08:38:45 +02:00
Romain Vimont
d14cc19c91 Add AOA gamepad support
Similar to AOA keyboard and mouse, but for gamepads.

Can be enabled with --gamepad=aoa.
2024-09-09 08:38:45 +02:00
Romain Vimont
385b31fb06 Implement HID gamepad
Implement the HID protocol for gamepads, that will be used in further
commits by the AOA and UHID gamepad processor implementations.
2024-09-09 08:38:45 +02:00
Romain Vimont
c4b6bb312c Add util functions to write in little-endian
This will be helpful for writing HID values.
2024-09-09 08:26:24 +02:00
Romain Vimont
abec04e8e7 Add connected gamepads on start
Trigger SDL_CONTROLLERDEVICEADDED for all gamepads already connected
when scrcpy starts. We want to handle both the gamepads initially
connected and the gamepads connected while scrcpy is running.

This is not racy, because this event may not trigged automatically until
SDL events are "pumped" (SDL_PumpEvents/SDL_WaitEvent).
2024-09-09 08:26:24 +02:00
Romain Vimont
0ea20f90be Handle SDL gamepad events
Introduce a gamepad processor trait, similar to the keyboard processor
and mouse processor traits.

Handle gamepad events received from SDL, convert them to scrcpy-specific
gamepad events, and forward them to the gamepad processor.

Further commits will provide AOA and UHID implementations of the gamepad
processor trait.

Co-authored-by: Luiz Henrique Laurini <luizhenriquelaurini@gmail.com>
2024-09-09 08:26:24 +02:00
Romain Vimont
a5ffe5b060 Fix HID comments
Fix typo and reference the latest version of "HID Usage Tables"
specifications.
2024-09-09 08:26:24 +02:00
Romain Vimont
6f1d79ba17 Make AOA keyboard/mouse open error fatal
Now that the AOA open/close are asynchronous, an open error did not make
scrcpy exit anymore.

Add a mechanism to exit if the AOA device could not be opened
asynchronously.
2024-09-09 08:26:24 +02:00
Romain Vimont
44e29989ee Unregister all AOA devices automatically on exit
Pushing a close event from the keyboard_aoa or mouse_aoa implementation
was racy, because the AOA thread might be stopped before these events
were processed.

Instead, keep the list of open AOA devices to close them automatically
from the AOA thread before exiting.
2024-09-09 08:26:24 +02:00
Romain Vimont
00786942be Make HID logs uniform 2024-09-09 08:26:24 +02:00
Romain Vimont
e6017cdc5d Add AOA open/close verbose logs 2024-09-09 08:26:24 +02:00
Romain Vimont
8fb87b5e6b Introduce hid_open and hid_close events
This allows to handle HID open/close at the same place as HID input
events (in the HID layer).

This will be especially useful to manage HID gamepads, to avoid
implementing one part in the HID layer and another part in the gamepad
processor implementation.
2024-09-09 08:26:24 +02:00
Romain Vimont
24f7ea5894 Rename hid_event to hid_input
The sc_hid_event structure represents HID input data. Rename it so that
we can add other hid event structs without confusion.
2024-09-09 08:26:24 +02:00
Romain Vimont
dbdfd9c8bf Make AOA open and close asynchronous
For AOA keyboard and mouse, only input events were asynchronous.
Register/unregister were called from the main thread.

This had the benefit to fail immediately if the AOA registration failed,
but to support gamepads we want to open/close AOA devices dynamically.

Also, it is better to avoid USB I/O from the main thread.
2024-09-09 08:26:24 +02:00
Romain Vimont
d58eb616f0 Reorder AOA functions
This will allow sc_aoa_setup_hid() to compile even when
sc_aoa_unregister_hid() will be made static.
2024-09-09 08:26:24 +02:00
Romain Vimont
230e6b4079 Refactor AOA handling
Extract event processing to a separate function.

This will make the code more readable when more event types will be
added.
2024-09-09 08:26:24 +02:00
Romain Vimont
3bd07aa8ff Move HID ids to common HID code
The HID ids (accessory ids or UHID ids) were defined by the keyboard and
mouse implementations.

Instead, define them in the common HID part, and make that id part of
the sc_hid_event.

This prepares the introduction of gamepad support, which will handle
several gamepads (and ids) in the common HID gamepad code.
2024-09-09 08:26:24 +02:00
Romain Vimont
28c91ecba2 Fix HID mouse header guard 2024-09-09 08:26:24 +02:00
Romain Vimont
9ebb836b20 Add missing SC_ prefix for HID mouse event size 2024-09-09 08:26:24 +02:00
Romain Vimont
1559940cee Remove duplicate definition SC_HID_MAX_SIZE
This constant is defined in hid_event.h.
2024-09-09 08:26:24 +02:00
Romain Vimont
0cc1a855dc Fail on AOA keyboard/mouse initialization error
If the AOA keyboard or the AOA mouse fails to be initialized, this is a
fatal error.
2024-09-09 08:26:24 +02:00
Romain Vimont
b49064064c Introduce non-droppable control messages
Control messages are queued from the main thread and sent to the device
from a separate thread.

When the queue is full, messages are just dropped. This avoids to
accumulate too much delay between the client and the device in case of
network issue.

However, some messages should not be dropped: for example, dropping a
UHID_CREATE message would make invalid all further UHID_INPUT messages.
Therefore, mark these messages as non-droppable.

A non-droppable event is queued anyway (resizing the queue if
necessary, unless the allocation fails).
2024-09-09 08:26:24 +02:00
Romain Vimont
ba342c398d Remove atomics from keyboard_uhid
The UHID output callback is now called from the same (main) thread as
the process_key() function.
2024-09-09 08:26:24 +02:00
Romain Vimont
d7fe119c8e Process UHID outputs events from the main thread
This will guarantee that the callbacks of UHID devices implementations
will always be called from the same (main) thread.
2024-09-09 08:26:24 +02:00
Romain Vimont
c5ccae5538 Set clipboard from the main thread
The clipboard changes from the device are received from a separate
thread, but it must be handled from the main thread.
2024-09-09 08:26:24 +02:00
Romain Vimont
87d9d68c07 Add mechanism to execute code on the main thread
This allows to schedule a runnable to be executed on the main thread,
until the event loop is explicitly terminated.

It is guaranteed that all accepted runnables will be executed (this
avoids possible memory leaks if a runnable owns resources).
2024-09-09 08:26:24 +02:00
Romain Vimont
684e2b632e Expose main thread id
This will allow to assert that a function is called from the main
thread.
2024-09-09 08:26:24 +02:00
Romain Vimont
8598e7d7a8 Extract sc_push_event()
Expose a convenience function to push an event without args to the main
thread.
2024-09-09 08:26:24 +02:00
Romain Vimont
8b372ae809 Store events numbers in an enum
This avoids to manually set an explicit value for each item.
2024-09-09 08:26:24 +02:00
Romain Vimont
28512d3872 Fix deprecated reference in scrcpy manpage
The options --hid-keyboard and --hid-mouse do not exist anymore. They
have been replaced by --keyboard=XXX and --mouse=XXX.
2024-09-09 08:26:24 +02:00
Romain Vimont
c11f07c1e8 Do not send uninitialized HID event
If the function returns false, then there is nothing to send.
2024-09-09 08:26:24 +02:00
Romain Vimont
903a5aaaf5 Replace "could not" by "cannot" where appropriate
"Could not" implies that the system tried to disable the option but
encountered an issue or failure.

"Cannot" indicates a rule or restriction, meaning it's not possible to
perform the action at all.
2024-09-09 08:24:52 +02:00
Romain Vimont
21b412cd98 Simplify messages reader/writer
In Java, control messages were parsed using manual buffering, which was
convoluted and error-prone.

Instead, read the socket directly through a DataInputStream and a
BufferedInputStream. Symmetrically, use a DataOutputStream and a
BufferedOutputStream to write messages.
2024-09-07 14:31:54 +02:00
29 changed files with 460 additions and 445 deletions

View File

@@ -26,6 +26,8 @@ _scrcpy() {
-e --select-tcpip
-f --fullscreen
--force-adb-forward
-G
--gamepad=
-h --help
-K
--keyboard=
@@ -127,6 +129,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
return
;;
--gamepad)
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
return
;;
--orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return

View File

@@ -33,8 +33,10 @@ arguments=(
{-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
{-h,--help}'[Print the help]'
'-K[Use UHID keyboard (same as --keyboard=uhid)]'
'-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
'--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]'
@@ -44,7 +46,7 @@ arguments=(
'--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID mouse (same as --mouse=uhid)]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]'
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse-bind=[Configure bindings of secondary clicks]'

View File

@@ -175,6 +175,10 @@ Start in fullscreen.
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
.TP
.B \-G
Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set.
.TP
.BI "\-\-gamepad " mode
Select how to send gamepad inputs to the device.
@@ -192,7 +196,7 @@ Print this help.
.TP
.B \-K
Same as \fB\-\-keyboard=uhid\fR.
Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set.
.TP
.BI "\-\-keyboard " mode
@@ -257,7 +261,7 @@ Default is 0 (unlimited).
.TP
.B \-M
Same as \fB\-\-mouse=uhid\fR.
Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set.
.TP
.BI "\-\-max\-fps " value

View File

@@ -375,7 +375,7 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'G',
.text = "Same as --gamepad=uhid.",
.text = "Same as --gamepad=uhid, or --gamepad=aoa if --otg is set.",
},
{
.longopt_id = OPT_GAMEPAD,
@@ -397,7 +397,7 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'K',
.text = "Same as --keyboard=uhid.",
.text = "Same as --keyboard=uhid, or --keyboard=aoa if --otg is set.",
},
{
.longopt_id = OPT_KEYBOARD,
@@ -493,7 +493,7 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'M',
.text = "Same as --mouse=uhid.",
.text = "Same as --mouse=uhid, or --mouse=aoa if --otg is set.",
},
{
.longopt_id = OPT_MAX_FPS,
@@ -2264,7 +2264,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
args->help = true;
break;
case 'K':
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID;
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA;
break;
case OPT_KEYBOARD:
if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) {
@@ -2286,7 +2286,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case 'M':
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID_OR_AOA;
break;
case OPT_MOUSE:
if (!parse_mouse(optarg, &opts->mouse_input_mode)) {
@@ -2671,7 +2671,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->audio_dup = true;
break;
case 'G':
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID;
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA;
break;
case OPT_GAMEPAD:
if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) {
@@ -2795,7 +2795,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_SDK;
} else if (opts->keyboard_input_mode
== SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_UHID;
}
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
@@ -2805,19 +2810,24 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
}
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) {
opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA
: SC_MOUSE_INPUT_MODE_UHID;
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK
&& !opts->video_playback) {
LOGE("SDK mouse mode requires video playback. Try --mouse=uhid.");
return false;
}
if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AUTO) {
if (otg) {
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_AOA;
} else {
// UHID does not work on all devices (with old Android
// versions), so it cannot be enabled by default
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED;
}
// UHID does not work on all devices (with old Android
// versions), so it cannot be enabled by default
opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA
: SC_GAMEPAD_INPUT_MODE_DISABLED;
} else if (opts->gamepad_input_mode
== SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) {
opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA
: SC_GAMEPAD_INPUT_MODE_UHID;
}
}
@@ -2875,9 +2885,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode;
if (gmode != SC_GAMEPAD_INPUT_MODE_AOA
&& gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
LOGE("In OTG mode, --gamepad only supports aoa or disabled.");
}
if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED) {
LOGE("Could not disable both keyboard and mouse in OTG mode.");
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED
&& gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) {
LOGE("Cannot not disable all inputs in OTG mode.");
return false;
}
}
@@ -2918,18 +2935,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
LOGE("Could not specify both --camera-id and --camera-facing");
LOGE("Cannot specify both --camera-id and --camera-facing");
return false;
}
if (opts->camera_size) {
if (opts->max_size) {
LOGE("Could not specify both --camera-size and -m/--max-size");
LOGE("Cannot specify both --camera-size and -m/--max-size");
return false;
}
if (opts->camera_ar) {
LOGE("Could not specify both --camera-size and --camera-ar");
LOGE("Cannot specify both --camera-size and --camera-ar");
return false;
}
}
@@ -3070,19 +3087,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (!opts->control) {
if (opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
LOGE("Cannot request to turn screen off if control is disabled");
return false;
}
if (opts->stay_awake) {
LOGE("Could not request to stay awake if control is disabled");
LOGE("Cannot request to stay awake if control is disabled");
return false;
}
if (opts->show_touches) {
LOGE("Could not request to show touches if control is disabled");
LOGE("Cannot request to show touches if control is disabled");
return false;
}
if (opts->power_off_on_close) {
LOGE("Could not request power off on close if control is disabled");
LOGE("Cannot request power off on close if control is disabled");
return false;
}
}
@@ -3107,7 +3124,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
// OTG mode is compatible with only very few options.
// Only report obvious errors.
if (opts->record_filename) {
LOGE("OTG mode: could not record");
LOGE("OTG mode: cannot record");
return false;
}
if (opts->turn_screen_off) {

View File

@@ -83,15 +83,34 @@ write_position(uint8_t *buf, const struct sc_position *position) {
sc_write16be(&buf[10], position->screen_size.height);
}
// write length (4 bytes) + string (non null-terminated)
// Write truncated string, and return the size
static size_t
write_string(const char *utf8, size_t max_len, uint8_t *buf) {
write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) {
if (!utf8) {
return 0;
}
size_t len = sc_str_utf8_truncation_index(utf8, max_len);
memcpy(payload, utf8, len);
return len;
}
// Write length (4 bytes) + string (non null-terminated)
static size_t
write_string(uint8_t *buf, const char *utf8, size_t max_len) {
size_t len = write_string_payload(buf + 4, utf8, max_len);
sc_write32be(buf, len);
memcpy(&buf[4], utf8, len);
return 4 + len;
}
// Write length (1 byte) + string (non null-terminated)
static size_t
write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) {
assert(max_len <= 0xFF);
size_t len = write_string_payload(buf + 1, utf8, max_len);
buf[0] = len;
return 1 + len;
}
size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
buf[0] = msg->type;
@@ -103,9 +122,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
sc_write32be(&buf[10], msg->inject_keycode.metastate);
return 14;
case SC_CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len =
write_string(msg->inject_text.text,
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]);
size_t len = write_string(&buf[1], msg->inject_text.text,
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
return 1 + len;
}
case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
@@ -137,19 +155,26 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
sc_write64be(&buf[1], msg->set_clipboard.sequence);
buf[9] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[10]);
size_t len = write_string(&buf[10], msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
return 10 + len;
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;
size_t index = 3;
index += write_string_tiny(&buf[index], msg->uhid_create.name, 127);
sc_write16be(&buf[index], msg->uhid_create.report_desc_size);
index += 2;
memcpy(&buf[index], msg->uhid_create.report_desc,
msg->uhid_create.report_desc_size);
index += msg->uhid_create.report_desc_size;
return index;
case SC_CONTROL_MSG_TYPE_UHID_INPUT:
sc_write16be(&buf[1], msg->uhid_input.id);
sc_write16be(&buf[3], msg->uhid_input.size);
@@ -255,10 +280,15 @@ 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);
case SC_CONTROL_MSG_TYPE_UHID_CREATE: {
// Quote only if name is not null
const char *name = msg->uhid_create.name;
const char *quote = name ? "\"" : "";
LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s "
"report_desc_size=%" PRIu16, msg->uhid_create.id,
quote, name, quote, 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);

View File

@@ -98,6 +98,7 @@ struct sc_control_msg {
} set_screen_power_mode;
struct {
uint16_t id;
const char *name; // pointer to static data
uint16_t report_desc_size;
const uint8_t *report_desc; // pointer to static data
} uhid_create;

View File

@@ -1,6 +1,7 @@
#include "events.h"
#include "util/log.h"
#include "util/thread.h"
bool
sc_push_event_impl(uint32_t type, const char *name) {
@@ -32,9 +33,34 @@ sc_post_to_main_thread(sc_runnable_fn run, void *userdata) {
// ret == 0: event was filtered
// ret == 1: success
if (ret != 1) {
LOGW("Coud not post to main thread: %s", SDL_GetError());
if (ret == 0) {
// if ret == 0, this is expected on exit, log in debug mode
LOGD("Could not post runnable to main thread (filtered)");
} else {
assert(ret < 0);
LOGW("Coud not post to main thread: %s", SDL_GetError());
}
return false;
}
return true;
}
static int SDLCALL
task_event_filter(void *userdata, SDL_Event *event) {
(void) userdata;
if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) {
// Reject this event type from now on
return 0;
}
return 1;
}
void
sc_reject_new_runnables(void) {
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
SDL_SetEventFilter(task_event_filter, NULL);
}

View File

@@ -1,5 +1,5 @@
#ifndef SC_EVENTS
#define SC_EVENTS
#ifndef SC_EVENTS_H
#define SC_EVENTS_H
#include "common.h"
@@ -32,4 +32,7 @@ typedef void (*sc_runnable_fn)(void *userdata);
bool
sc_post_to_main_thread(sc_runnable_fn run, void *userdata);
void
sc_reject_new_runnables(void);
#endif

View File

@@ -15,6 +15,7 @@ struct sc_hid_input {
struct sc_hid_open {
uint16_t hid_id;
const char *name; // pointer to static memory
const uint8_t *report_desc; // pointer to static memory
size_t report_desc_size;
};

View File

@@ -76,8 +76,8 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
0x09, 0xC4,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (65535)
0x27, 0xFF, 0xFF, 0x00, 0x00,
// Logical Maximum (32767)
0x26, 0xFF, 0x7F,
// Report Size (16)
0x75, 0x10,
// Report Count (2)
@@ -89,26 +89,19 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
0x05, 0x09,
// Usage Minimum (1)
0x19, 0x01,
// Usage Maximum (15)
0x29, 0x0E,
// Usage Maximum (16)
0x29, 0x10,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (1)
0x25, 0x01,
// Report Count (15)
0x95, 0x0F,
// Report Count (16)
0x95, 0x10,
// Report Size (1)
0x75, 0x01,
// Input (Data, Variable, Absolute): 15 buttons bits
// Input (Data, Variable, Absolute): 16 buttons bits
0x81, 0x02,
// Report Count (1)
0x95, 0x01,
// Report Size (1)
0x75, 0x01,
// Input (Constant): 1-bit padding
0x81, 0x01,
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Hat switch)
@@ -251,8 +244,14 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id);
SDL_GameController* game_controller =
SDL_GameControllerFromInstanceID(gamepad_id);
assert(game_controller);
const char *name = SDL_GameControllerName(game_controller);
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
hid_open->hid_id = hid_id;
hid_open->name = name;
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
@@ -320,14 +319,13 @@ sc_hid_gamepad_event_from_slot(uint16_t hid_id,
hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE;
uint8_t *data = hid_input->data;
// Buttons are individual bits in the report descriptor, so the resulting
// u16 must be written in little-endian
sc_write16be(data, slot->axis_left_x);
sc_write16be(data + 2, slot->axis_left_y);
sc_write16be(data + 4, slot->axis_right_x);
sc_write16be(data + 6, slot->axis_right_y);
sc_write16be(data + 8, slot->axis_left_trigger);
sc_write16be(data + 10, slot->axis_right_trigger);
// Values must be written in little-endian
sc_write16le(data, slot->axis_left_x);
sc_write16le(data + 2, slot->axis_left_y);
sc_write16le(data + 4, slot->axis_right_x);
sc_write16le(data + 6, slot->axis_right_y);
sc_write16le(data + 8, slot->axis_left_trigger);
sc_write16le(data + 10, slot->axis_right_trigger);
sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK);
data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons);
}
@@ -406,6 +404,7 @@ sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid,
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input);
return true;
}
@@ -425,26 +424,28 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx];
// [-32768 to 32767] -> [0 to 65535]
uint16_t value = ((int32_t) event->value) + 0x8000;
// [-32768 to 32767] -> [0 to 65535]
#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000)
switch (event->axis) {
case SC_GAMEPAD_AXIS_LEFTX:
slot->axis_left_x = value;
slot->axis_left_x = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_LEFTY:
slot->axis_left_y = value;
slot->axis_left_y = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_RIGHTX:
slot->axis_right_x = value;
slot->axis_right_x = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_RIGHTY:
slot->axis_right_y = value;
slot->axis_right_y = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_LEFT_TRIGGER:
slot->axis_left_trigger = value;
// Trigger is always positive between 0 and 32767
slot->axis_left_trigger = MAX(0, event->value);
break;
case SC_GAMEPAD_AXIS_RIGHT_TRIGGER:
slot->axis_right_trigger = value;
// Trigger is always positive between 0 and 32767
slot->axis_right_trigger = MAX(0, event->value);
break;
default:
return false;
@@ -452,5 +453,6 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input);
return true;
}

View File

@@ -9,7 +9,7 @@
#include "input_events.h"
#define SC_MAX_GAMEPADS 8
#define SC_HID_ID_GAMEPAD_FIRST 2
#define SC_HID_ID_GAMEPAD_FIRST 3
#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1)
struct sc_hid_gamepad_slot {

View File

@@ -335,6 +335,7 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
hid_open->hid_id = SC_HID_ID_KEYBOARD;
hid_open->name = "Keyboard";
hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
}

View File

@@ -190,6 +190,7 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
hid_open->hid_id = SC_HID_ID_MOUSE;
hid_open->name = "Mouse";
hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC);
}

View File

@@ -142,6 +142,7 @@ enum sc_lock_video_orientation {
enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
SC_KEYBOARD_INPUT_MODE_DISABLED,
SC_KEYBOARD_INPUT_MODE_SDK,
SC_KEYBOARD_INPUT_MODE_UHID,
@@ -150,6 +151,7 @@ enum sc_keyboard_input_mode {
enum sc_mouse_input_mode {
SC_MOUSE_INPUT_MODE_AUTO,
SC_MOUSE_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
SC_MOUSE_INPUT_MODE_DISABLED,
SC_MOUSE_INPUT_MODE_SDK,
SC_MOUSE_INPUT_MODE_UHID,
@@ -158,6 +160,7 @@ enum sc_mouse_input_mode {
enum sc_gamepad_input_mode {
SC_GAMEPAD_INPUT_MODE_AUTO,
SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
SC_GAMEPAD_INPUT_MODE_DISABLED,
SC_GAMEPAD_INPUT_MODE_UHID,
SC_GAMEPAD_INPUT_MODE_AOA,

View File

@@ -200,6 +200,21 @@ event_loop(struct scrcpy *s) {
return SCRCPY_EXIT_FAILURE;
}
static void
terminate_event_loop(void) {
sc_reject_new_runnables();
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) {
// Make sure all posted runnables are run, to avoid memory leaks
sc_runnable_fn run = event.user.data1;
void *userdata = event.user.data2;
run(userdata);
}
}
}
// Return true on success, false on error
static bool
await_for_server(bool *connected) {
@@ -741,6 +756,7 @@ aoa_complete:
if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) {
sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller);
gp = &s->gamepad_uhid.gamepad_processor;
uhid_gamepad = &s->gamepad_uhid;
}
struct sc_uhid_devices *uhid_devices = NULL;
@@ -888,6 +904,7 @@ aoa_complete:
}
ret = event_loop(s);
terminate_event_loop();
LOGD("quit...");
if (options->video_playback) {

View File

@@ -31,6 +31,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad,
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = hid_open->hid_id;
msg.uhid_create.name = hid_open->name;
msg.uhid_create.report_desc = hid_open->report_desc;
msg.uhid_create.report_desc_size = hid_open->report_desc_size;
@@ -83,7 +84,10 @@ sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp,
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, event);
if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input,
event)) {
return;
}
sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad axis");
}
@@ -94,7 +98,10 @@ sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp,
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, event);
if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input,
event)) {
return;
}
sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button");

View File

@@ -38,7 +38,9 @@ sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) {
kb->device_mod = mod;
struct sc_hid_input hid_input;
sc_hid_keyboard_generate_input_from_mods(&hid_input, diff);
if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, diff)) {
return;
}
LOGV("HID keyboard state synchronized");
@@ -139,6 +141,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = SC_HID_ID_KEYBOARD;
msg.uhid_create.name = hid_open.name;
msg.uhid_create.report_desc = hid_open.report_desc;
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
if (!sc_controller_push_msg(controller, &msg)) {

View File

@@ -81,6 +81,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = SC_HID_ID_MOUSE;
msg.uhid_create.name = hid_open.name;
msg.uhid_create.report_desc = hid_open.report_desc;
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
if (!sc_controller_push_msg(controller, &msg)) {

View File

@@ -42,11 +42,6 @@ event_loop(struct scrcpy_otg *s) {
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
case SC_EVENT_RUN_ON_MAIN_THREAD:
sc_runnable_fn run = event.user.data1;
void *userdata = event.user.data2;
run(userdata);
break;
default:
sc_screen_otg_handle_event(&s->screen_otg, &event);
break;

View File

@@ -329,6 +329,7 @@ static void test_serialize_uhid_create(void) {
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
.uhid_create = {
.id = 42,
.name = "ABC",
.report_desc_size = sizeof(report_desc),
.report_desc = report_desc,
},
@@ -336,12 +337,14 @@ static void test_serialize_uhid_create(void) {
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 16);
assert(size == 20);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_CREATE,
0, 42, // id
0, 11, // size
3, // name size
65, 66, 67, // "ABC"
0, 11, // report desc size
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
};
assert(!memcmp(buf, expected, sizeof(expected)));

View File

@@ -3,31 +3,22 @@ package com.genymobile.scrcpy.control;
import android.net.LocalSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public final class ControlChannel {
private final InputStream inputStream;
private final OutputStream outputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private final ControlMessageReader reader;
private final DeviceMessageWriter writer;
public ControlChannel(LocalSocket controlSocket) throws IOException {
this.inputStream = controlSocket.getInputStream();
this.outputStream = controlSocket.getOutputStream();
reader = new ControlMessageReader(controlSocket.getInputStream());
writer = new DeviceMessageWriter(controlSocket.getOutputStream());
}
public ControlMessage recv() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(inputStream);
msg = reader.next();
}
return msg;
return reader.read();
}
public void send(DeviceMessage msg) throws IOException {
writer.writeTo(msg, outputStream);
writer.write(msg);
}
}

View File

@@ -131,10 +131,11 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createUhidCreate(int id, byte[] reportDesc) {
public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_UHID_CREATE;
msg.id = id;
msg.text = name;
msg.data = reportDesc;
return msg;
}

View File

@@ -1,271 +1,166 @@
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Binary;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Position;
import java.io.EOFException;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
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;
static final int UHID_DESTROY_PAYLOAD_LENGTH = 2;
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
public static final int INJECT_TEXT_MAX_LENGTH = 300;
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final DataInputStream dis;
public ControlMessageReader() {
// invariant: the buffer is always in "get" mode
buffer.limit(0);
public ControlMessageReader(InputStream rawInputStream) {
dis = new DataInputStream(new BufferedInputStream(rawInputStream));
}
public boolean isFull() {
return buffer.remaining() == rawBuffer.length;
}
public void readFrom(InputStream input) throws IOException {
if (isFull()) {
throw new IllegalStateException("Buffer full, call next() to consume");
}
buffer.compact();
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
throw new EOFException("Controller socket closed");
}
buffer.position(head + r);
buffer.flip();
}
public ControlMessage next() {
if (!buffer.hasRemaining()) {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlMessage msg;
public ControlMessage read() throws IOException {
int type = dis.readUnsignedByte();
switch (type) {
case ControlMessage.TYPE_INJECT_KEYCODE:
msg = parseInjectKeycode();
break;
return parseInjectKeycode();
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
return parseInjectText();
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
msg = parseInjectTouchEvent();
break;
return parseInjectTouchEvent();
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
return parseInjectScrollEvent();
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
msg = parseBackOrScreenOnEvent();
break;
return parseBackOrScreenOnEvent();
case ControlMessage.TYPE_GET_CLIPBOARD:
msg = parseGetClipboard();
break;
return parseGetClipboard();
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
break;
return parseSetClipboard();
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode();
break;
return parseSetScreenPowerMode();
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
msg = ControlMessage.createEmpty(type);
break;
return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE:
msg = parseUhidCreate();
break;
return parseUhidCreate();
case ControlMessage.TYPE_UHID_INPUT:
msg = parseUhidInput();
break;
return parseUhidInput();
case ControlMessage.TYPE_UHID_DESTROY:
msg = parseUhidDestroy();
break;
return parseUhidDestroy();
default:
Ln.w("Unknown event type: " + type);
msg = null;
break;
throw new ControlProtocolException("Unknown event type: " + type);
}
if (msg == null) {
// failure, reset savedPosition
buffer.position(savedPosition);
}
return msg;
}
private ControlMessage parseInjectKeycode() {
if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) {
return null;
}
int action = Binary.toUnsigned(buffer.get());
int keycode = buffer.getInt();
int repeat = buffer.getInt();
int metaState = buffer.getInt();
private ControlMessage parseInjectKeycode() throws IOException {
int action = dis.readUnsignedByte();
int keycode = dis.readInt();
int repeat = dis.readInt();
int metaState = dis.readInt();
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
}
private int parseBufferLength(int sizeBytes) {
private int parseBufferLength(int sizeBytes) throws IOException {
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);
value = (value << 8) | dis.readUnsignedByte();
}
return value;
}
private String parseString() {
int len = parseBufferLength(4);
if (len == -1 || buffer.remaining() < len) {
return null;
}
int position = buffer.position();
// Move the buffer position to consume the text
buffer.position(position + len);
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
private String parseString(int sizeBytes) throws IOException {
assert sizeBytes > 0 && sizeBytes <= 4;
byte[] data = parseByteArray(sizeBytes);
return new String(data, StandardCharsets.UTF_8);
}
private byte[] parseByteArray(int sizeBytes) {
private String parseString() throws IOException {
byte[] data = parseByteArray(4);
return new String(data, StandardCharsets.UTF_8);
}
private byte[] parseByteArray(int sizeBytes) throws IOException {
int len = parseBufferLength(sizeBytes);
if (len == -1 || buffer.remaining() < len) {
return null;
}
byte[] data = new byte[len];
buffer.get(data);
dis.readFully(data);
return data;
}
private ControlMessage parseInjectText() {
private ControlMessage parseInjectText() throws IOException {
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectTouchEvent() {
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
return null;
}
int action = Binary.toUnsigned(buffer.get());
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
int actionButton = buffer.getInt();
int buttons = buffer.getInt();
private ControlMessage parseInjectTouchEvent() throws IOException {
int action = dis.readUnsignedByte();
long pointerId = dis.readLong();
Position position = parsePosition();
float pressure = Binary.u16FixedPointToFloat(dis.readShort());
int actionButton = dis.readInt();
int buttons = dis.readInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
}
private ControlMessage parseInjectScrollEvent() {
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
float hScroll = Binary.i16FixedPointToFloat(buffer.getShort());
float vScroll = Binary.i16FixedPointToFloat(buffer.getShort());
int buttons = buffer.getInt();
private ControlMessage parseInjectScrollEvent() throws IOException {
Position position = parsePosition();
float hScroll = Binary.i16FixedPointToFloat(dis.readShort());
float vScroll = Binary.i16FixedPointToFloat(dis.readShort());
int buttons = dis.readInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
}
private ControlMessage parseBackOrScreenOnEvent() {
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
return null;
}
int action = Binary.toUnsigned(buffer.get());
private ControlMessage parseBackOrScreenOnEvent() throws IOException {
int action = dis.readUnsignedByte();
return ControlMessage.createBackOrScreenOn(action);
}
private ControlMessage parseGetClipboard() {
if (buffer.remaining() < GET_CLIPBOARD_LENGTH) {
return null;
}
int copyKey = Binary.toUnsigned(buffer.get());
private ControlMessage parseGetClipboard() throws IOException {
int copyKey = dis.readUnsignedByte();
return ControlMessage.createGetClipboard(copyKey);
}
private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;
}
long sequence = buffer.getLong();
boolean paste = buffer.get() != 0;
private ControlMessage parseSetClipboard() throws IOException {
long sequence = dis.readLong();
boolean paste = dis.readByte() != 0;
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(sequence, text, paste);
}
private ControlMessage parseSetScreenPowerMode() {
if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) {
return null;
}
int mode = buffer.get();
private ControlMessage parseSetScreenPowerMode() throws IOException {
int mode = dis.readUnsignedByte();
return ControlMessage.createSetScreenPowerMode(mode);
}
private ControlMessage parseUhidCreate() {
if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) {
return null;
}
int id = buffer.getShort();
private ControlMessage parseUhidCreate() throws IOException {
int id = dis.readUnsignedShort();
String name = parseString(1);
byte[] data = parseByteArray(2);
if (data == null) {
return null;
}
return ControlMessage.createUhidCreate(id, data);
return ControlMessage.createUhidCreate(id, name, data);
}
private ControlMessage parseUhidInput() {
if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) {
return null;
}
int id = buffer.getShort();
private ControlMessage parseUhidInput() throws IOException {
int id = dis.readUnsignedShort();
byte[] data = parseByteArray(2);
if (data == null) {
return null;
}
return ControlMessage.createUhidInput(id, data);
}
private ControlMessage parseUhidDestroy() {
if (buffer.remaining() < UHID_DESTROY_PAYLOAD_LENGTH) {
return null;
}
int id = buffer.getShort();
private ControlMessage parseUhidDestroy() throws IOException {
int id = dis.readUnsignedShort();
return ControlMessage.createUhidDestroy(id);
}
private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = Binary.toUnsigned(buffer.getShort());
int screenHeight = Binary.toUnsigned(buffer.getShort());
private Position parsePosition() throws IOException {
int x = dis.readInt();
int y = dis.readInt();
int screenWidth = dis.readUnsignedShort();
int screenHeight = dis.readUnsignedShort();
return new Position(x, y, screenWidth, screenHeight);
}
}

View File

@@ -0,0 +1,9 @@
package com.genymobile.scrcpy.control;
import java.io.IOException;
public class ControlProtocolException extends IOException {
public ControlProtocolException(String message) {
super(message);
}
}

View File

@@ -210,7 +210,7 @@ public class Controller implements AsyncProcessor {
device.rotateDevice();
break;
case ControlMessage.TYPE_UHID_CREATE:
getUhidManager().open(msg.getId(), msg.getData());
getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
break;
case ControlMessage.TYPE_UHID_INPUT:
getUhidManager().writeInput(msg.getId(), msg.getData());

View File

@@ -1,11 +1,11 @@
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter {
@@ -13,35 +13,35 @@ public class DeviceMessageWriter {
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final DataOutputStream dos;
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
buffer.clear();
buffer.put((byte) msg.getType());
switch (msg.getType()) {
public DeviceMessageWriter(OutputStream rawOutputStream) {
dos = new DataOutputStream(new BufferedOutputStream(rawOutputStream));
}
public void write(DeviceMessage msg) throws IOException {
int type = msg.getType();
dos.writeByte(type);
switch (type) {
case DeviceMessage.TYPE_CLIPBOARD:
String text = msg.getText();
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
buffer.putInt(len);
buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position());
dos.writeInt(len);
dos.write(raw, 0, len);
break;
case DeviceMessage.TYPE_ACK_CLIPBOARD:
buffer.putLong(msg.getSequence());
output.write(rawBuffer, 0, buffer.position());
dos.writeLong(msg.getSequence());
break;
case DeviceMessage.TYPE_UHID_OUTPUT:
buffer.putShort((short) msg.getId());
dos.writeShort(msg.getId());
byte[] data = msg.getData();
buffer.putShort((short) data.length);
buffer.put(data);
output.write(rawBuffer, 0, buffer.position());
dos.writeShort(data.length);
dos.write(data);
break;
default:
Ln.w("Unknown device message: " + msg.getType());
break;
throw new ControlProtocolException("Unknown event type: " + type);
}
dos.flush();
}
}

View File

@@ -1,6 +1,7 @@
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
import android.os.Build;
import android.os.HandlerThread;
@@ -46,7 +47,7 @@ public final class UhidManager {
}
}
public void open(int id, byte[] reportDesc) throws IOException {
public void open(int id, String name, byte[] reportDesc) throws IOException {
try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try {
@@ -56,7 +57,7 @@ public final class UhidManager {
close(old);
}
byte[] req = buildUhidCreate2Req(reportDesc);
byte[] req = buildUhidCreate2Req(name, reportDesc);
Os.write(fd, req, 0, req.length);
registerUhidListener(id, fd);
@@ -146,7 +147,7 @@ public final class UhidManager {
}
}
private static byte[] buildUhidCreate2Req(byte[] reportDesc) {
private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) {
/*
* struct uhid_event {
* uint32_t type;
@@ -171,8 +172,19 @@ public final class UhidManager {
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());
final String prefix = "scrcpy: ";
if (name.isEmpty()) {
name = "(no name)";
}
byte[] utf8Name = name.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127 - prefix.length());
int nameLen = prefix.length() + len;
assert nameLen <= 127;
buf.put(prefix.getBytes(StandardCharsets.US_ASCII));
buf.put(utf8Name, 0, len);
buf.put(empty, 0, 256 - nameLen);
buf.putShort((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL);
buf.putInt(0); // vendor id

View File

@@ -10,6 +10,7 @@ import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -18,8 +19,6 @@ public class ControlMessageReaderTest {
@Test
public void testParseKeycodeEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
@@ -29,23 +28,21 @@ public class ControlMessageReaderTest {
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseTextEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
@@ -54,17 +51,18 @@ public class ControlMessageReaderTest {
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
Assert.assertEquals("testé", event.getText());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseLongTextEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
@@ -74,17 +72,18 @@ public class ControlMessageReaderTest {
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseTouchEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT);
@@ -100,12 +99,10 @@ public class ControlMessageReaderTest {
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(-42, event.getPointerId());
@@ -116,12 +113,12 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseScrollEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT);
@@ -132,15 +129,12 @@ public class ControlMessageReaderTest {
dos.writeShort(0); // 0.0f encoded as i16
dos.writeShort(0x8000); // -1.0f encoded as i16
dos.writeInt(1);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType());
Assert.assertEquals(260, event.getPosition().getPoint().getX());
Assert.assertEquals(1026, event.getPosition().getPoint().getY());
@@ -149,96 +143,96 @@ public class ControlMessageReaderTest {
Assert.assertEquals(0f, event.getHScroll(), 0f);
Assert.assertEquals(-1f, event.getVScroll(), 0f);
Assert.assertEquals(1, event.getButtons());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseBackOrScreenOnEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
dos.writeByte(KeyEvent.ACTION_UP);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseExpandNotificationPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseExpandSettingsPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseCollapsePanelsEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseGetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
dos.writeByte(ControlMessage.COPY_KEY_COPY);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseSetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
@@ -247,22 +241,22 @@ public class ControlMessageReaderTest {
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0102030405060708L, event.getSequence());
Assert.assertEquals("testé", event.getText());
Assert.assertTrue(event.getPaste());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseBigSetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
@@ -278,78 +272,79 @@ public class ControlMessageReaderTest {
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0807060504030201L, event.getSequence());
Assert.assertEquals(text, event.getText());
Assert.assertTrue(event.getPaste());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseSetScreenPowerMode() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE);
dos.writeByte(Device.POWER_MODE_NORMAL);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType());
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseRotateDevice() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@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
dos.writeByte(3); // name size
dos.write("ABC".getBytes(StandardCharsets.US_ASCII));
byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
dos.writeShort(data.length); // size
dos.writeShort(data.length); // report desc size
dos.write(data);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType());
Assert.assertEquals(42, event.getId());
Assert.assertEquals("ABC", event.getText());
Assert.assertArrayEquals(data, event.getData());
Assert.assertEquals(-1, bis.read()); // EOS
}
@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);
@@ -357,55 +352,55 @@ public class ControlMessageReaderTest {
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();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType());
Assert.assertEquals(42, event.getId());
Assert.assertArrayEquals(data, event.getData());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseUhidDestroy() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_UHID_DESTROY);
dos.writeShort(42); // id
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_UHID_DESTROY, event.getType());
Assert.assertEquals(42, event.getId());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseOpenHardKeyboardSettings() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
@@ -422,27 +417,29 @@ public class ControlMessageReaderTest {
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(0, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(1, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testPartialEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
@@ -456,31 +453,21 @@ public class ControlMessageReaderTest {
dos.writeByte(MotionEvent.ACTION_DOWN);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.next();
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(4, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertNull(event); // the event is not complete
bos.reset();
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON);
packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
// the event is now complete
event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
try {
event = reader.read();
Assert.fail("Reader did not reach EOF");
} catch (EOFException e) {
// expected
}
}
}

View File

@@ -12,8 +12,6 @@ public class DeviceMessageWriterTest {
@Test
public void testSerializeClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
String text = "aéûoç";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -21,12 +19,13 @@ public class DeviceMessageWriterTest {
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeInt(data.length);
dos.write(data);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createClipboard(text);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
DeviceMessageWriter writer = new DeviceMessageWriter(bos);
DeviceMessage msg = DeviceMessage.createClipboard(text);
writer.write(msg);
byte[] actual = bos.toByteArray();
@@ -35,18 +34,17 @@ public class DeviceMessageWriterTest {
@Test
public void testSerializeAckSetClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD);
dos.writeLong(0x0102030405060708L);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
DeviceMessageWriter writer = new DeviceMessageWriter(bos);
DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L);
writer.write(msg);
byte[] actual = bos.toByteArray();
@@ -55,8 +53,6 @@ public class DeviceMessageWriterTest {
@Test
public void testSerializeUhidOutput() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT);
@@ -64,12 +60,13 @@ public class DeviceMessageWriterTest {
byte[] data = {1, 2, 3, 4, 5};
dos.writeShort(data.length);
dos.write(data);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createUhidOutput(42, data);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
DeviceMessageWriter writer = new DeviceMessageWriter(bos);
DeviceMessage msg = DeviceMessage.createUhidOutput(42, data);
writer.write(msg);
byte[] actual = bos.toByteArray();