diff --git a/.gitignore b/.gitignore index 222769b3..7bc30289 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ build/ /dist/ +/build-*/ +/build_*/ +/release-*/ .idea/ .gradle/ /x/ diff --git a/BUILD.md b/BUILD.md index 57b2db97..2f1c2be9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.14`][direct-scrcpy-server] - _(SHA-256: 1d1b18a2b80e956771fd63b99b414d2d028713a8f12ddfa5a369709ad4295620)_ + - [`scrcpy-server-v1.15.1`][direct-scrcpy-server] + _(SHA-256: fe06bd6a30da8c89860bf5e16eecce2b5054d4644c84289670ce00ca5d1637c3)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-server-v1.14 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15.1/scrcpy-server-v1.15.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 3aed2019..0415af82 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/app/meson.build b/app/meson.build index 5d2b4caa..0163dd7f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -164,12 +164,12 @@ if get_option('buildtype') == 'debug' 'src/cli.c', 'src/util/str_util.c', ]], - ['test_control_event_serialize', [ + ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', 'src/util/str_util.c', ]], - ['test_device_event_deserialize', [ + ['test_device_msg_deserialize', [ 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], @@ -186,7 +186,7 @@ if get_option('buildtype') == 'debug' exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies, - c_args: ['-DSDL_MAIN_HANDLED']) + c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) test(t[0], exe) endforeach endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 776d78ae..49fa78dc 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -43,6 +43,10 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-disable-screensaver" +Disable screensaver while scrcpy is running. + .TP .BI "\-\-display " id Specify the display id to mirror. @@ -92,6 +96,10 @@ Do not display device (only when screen recording is enabled). .B \-\-no\-mipmaps If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. +.TP +.B \-\-no\-key\-repeat +Do not forward repeated key events when a key is held down. + .TP .BI "\-p, \-\-port " port[:port] Set the TCP port (range) used by the client to listen. @@ -145,6 +153,16 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. +.TP +.BI "\-\-shortcut\-mod " key[+...]][,...] +Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". + +A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. + +For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". + +Default is "lalt,lsuper" (left-Alt or left-Super). + .TP .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. @@ -203,52 +221,55 @@ Default is 0 (automatic).\n .SH SHORTCUTS +In the following list, MOD is the shortcut modifier. By default, it's (left) +Alt or (left) Super, but it can be configured by \-\-shortcut-mod. + .TP -.B Ctrl+f +.B MOD+f Switch fullscreen mode .TP -.B Ctrl+Left +.B MOD+Left Rotate display left .TP -.B Ctrl+Right +.B MOD+Right Rotate display right .TP -.B Ctrl+g +.B MOD+g Resize window to 1:1 (pixel\-perfect) .TP -.B Ctrl+x, Double\-click on black borders +.B MOD+w, Double\-click on black borders Resize window to remove black borders .TP -.B Ctrl+h, Home, Middle\-click +.B MOD+h, Home, Middle\-click Click on HOME .TP -.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) +.B MOD+b, MOD+Backspace, Right\-click (when screen is on) Click on BACK .TP -.B Ctrl+s +.B MOD+s Click on APP_SWITCH .TP -.B Ctrl+m +.B MOD+m Click on MENU .TP -.B Ctrl+Up +.B MOD+Up Click on VOLUME_UP .TP -.B Ctrl+Down +.B MOD+Down Click on VOLUME_DOWN .TP -.B Ctrl+p +.B MOD+p Click on POWER (turn screen on/off) .TP @@ -256,39 +277,43 @@ Click on POWER (turn screen on/off) Turn screen on .TP -.B Ctrl+o +.B MOD+o Turn device screen off (keep mirroring) .TP -.B Ctrl+Shift+o +.B MOD+Shift+o Turn device screen on .TP -.B Ctrl+r +.B MOD+r Rotate device screen .TP -.B Ctrl+n +.B MOD+n Expand notification panel .TP -.B Ctrl+Shift+n +.B MOD+Shift+n Collapse notification panel .TP -.B Ctrl+c -Copy device clipboard to computer +.B Mod+c +Copy to clipboard (inject COPY keycode, Android >= 7 only) .TP -.B Ctrl+v -Paste computer clipboard to device +.B Mod+x +Cut to clipboard (inject CUT keycode, Android >= 7 only) .TP -.B Ctrl+Shift+v -Copy computer clipboard to device (and paste if the device runs Android >= 7) +.B MOD+v +Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) .TP -.B Ctrl+i +.B MOD+Shift+v +Inject computer clipboard text as a sequence of key events + +.TP +.B MOD+i Enable/disable FPS counter (print frames/second in logs) .TP diff --git a/app/src/cli.c b/app/src/cli.c index be0b7c23..c957b22f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -3,20 +3,16 @@ #include #include #include +#include #include #include "config.h" -#include "recorder.h" +#include "scrcpy.h" #include "util/log.h" #include "util/str_util.h" void scrcpy_print_usage(const char *arg0) { -#ifdef __APPLE__ -# define CTRL_OR_CMD "Cmd" -#else -# define CTRL_OR_CMD "Ctrl" -#endif fprintf(stderr, "Usage: %s [options]\n" "\n" @@ -45,6 +41,9 @@ scrcpy_print_usage(const char *arg0) { " (typically, portrait for a phone, landscape for a tablet).\n" " Any --max-size value is computed on the cropped size.\n" "\n" + " --disable-screensaver\n" + " Disable screensaver while scrcpy is running.\n" + "\n" " --display id\n" " Specify the display id to mirror.\n" "\n" @@ -93,6 +92,9 @@ scrcpy_print_usage(const char *arg0) { " mipmaps are automatically generated to improve downscaling\n" " quality. This option disables the generation of mipmaps.\n" "\n" + " --no-key-repeat\n" + " Do not forward repeated key events when a key is held down.\n" + "\n" " -p, --port port[:port]\n" " Set the TCP port (range) used by the client to listen.\n" " Default is %d:%d.\n" @@ -139,6 +141,19 @@ scrcpy_print_usage(const char *arg0) { " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" "\n" + " --shortcut-mod key[+...]][,...]\n" + " Specify the modifiers to use for scrcpy shortcuts.\n" + " Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n" + " \"lsuper\" and \"rsuper\".\n" + "\n" + " A shortcut can consist in several keys, separated by '+'.\n" + " Several shortcuts can be specified, separated by ','.\n" + "\n" + " For example, to use either LCtrl+LAlt or LSuper for scrcpy\n" + " shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "\n" + " Default is \"lalt,lsuper\" (left-Alt or left-Super).\n" + "\n" " -S, --turn-screen-off\n" " Turn the device screen off immediately.\n" "\n" @@ -186,75 +201,82 @@ scrcpy_print_usage(const char *arg0) { "\n" "Shortcuts:\n" "\n" - " " CTRL_OR_CMD "+f\n" + " In the following list, MOD is the shortcut modifier. By default,\n" + " it's (left) Alt or (left) Super, but it can be configured by\n" + " --shortcut-mod.\n" + "\n" + " MOD+f\n" " Switch fullscreen mode\n" "\n" - " " CTRL_OR_CMD "+Left\n" + " MOD+Left\n" " Rotate display left\n" "\n" - " " CTRL_OR_CMD "+Right\n" + " MOD+Right\n" " Rotate display right\n" "\n" - " " CTRL_OR_CMD "+g\n" + " MOD+g\n" " Resize window to 1:1 (pixel-perfect)\n" "\n" - " " CTRL_OR_CMD "+x\n" + " MOD+w\n" " Double-click on black borders\n" " Resize window to remove black borders\n" "\n" - " Ctrl+h\n" + " MOD+h\n" " Middle-click\n" " Click on HOME\n" "\n" - " " CTRL_OR_CMD "+b\n" - " " CTRL_OR_CMD "+Backspace\n" + " MOD+b\n" + " MOD+Backspace\n" " Right-click (when screen is on)\n" " Click on BACK\n" "\n" - " " CTRL_OR_CMD "+s\n" + " MOD+s\n" " Click on APP_SWITCH\n" "\n" - " Ctrl+m\n" + " MOD+m\n" " Click on MENU\n" "\n" - " " CTRL_OR_CMD "+Up\n" + " MOD+Up\n" " Click on VOLUME_UP\n" "\n" - " " CTRL_OR_CMD "+Down\n" + " MOD+Down\n" " Click on VOLUME_DOWN\n" "\n" - " " CTRL_OR_CMD "+p\n" + " MOD+p\n" " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" " Power on\n" "\n" - " " CTRL_OR_CMD "+o\n" + " MOD+o\n" " Turn device screen off (keep mirroring)\n" "\n" - " " CTRL_OR_CMD "+Shift+o\n" + " MOD+Shift+o\n" " Turn device screen on\n" "\n" - " " CTRL_OR_CMD "+r\n" + " MOD+r\n" " Rotate device screen\n" "\n" - " " CTRL_OR_CMD "+n\n" + " MOD+n\n" " Expand notification panel\n" "\n" - " " CTRL_OR_CMD "+Shift+n\n" + " MOD+Shift+n\n" " Collapse notification panel\n" "\n" - " " CTRL_OR_CMD "+c\n" - " Copy device clipboard to computer\n" + " MOD+c\n" + " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n" "\n" - " " CTRL_OR_CMD "+v\n" - " Paste computer clipboard to device\n" + " MOD+x\n" + " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n" "\n" - " " CTRL_OR_CMD "+Shift+v\n" - " Copy computer clipboard to device (and paste if the device\n" - " runs Android >= 7)\n" + " MOD+v\n" + " Copy computer clipboard to device, then paste (inject PASTE\n" + " keycode, Android >= 7 only)\n" "\n" - " " CTRL_OR_CMD "+i\n" + " MOD+Shift+v\n" + " Inject computer clipboard text as a sequence of key events\n" + "\n" + " MOD+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" @@ -379,10 +401,10 @@ parse_rotation(const char *s, uint8_t *rotation) { static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" - static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); + static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); if (!strcmp(s, "auto")) { - *position = WINDOW_POSITION_UNDEFINED; + *position = SC_WINDOW_POSITION_UNDEFINED; return true; } @@ -411,7 +433,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) { } static bool -parse_port_range(const char *s, struct port_range *port_range) { +parse_port_range(const char *s, struct sc_port_range *port_range) { long values[2]; size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); if (!count) { @@ -476,21 +498,116 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { return false; } +// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt") +// returns a bitwise-or of SC_MOD_* constants (or 0 on error) +static unsigned +parse_shortcut_mods_item(const char *item, size_t len) { + unsigned mod = 0; + + for (;;) { + char *plus = strchr(item, '+'); + // strchr() does not consider the "len" parameter, to it could find an + // occurrence too far in the string (there is no strnchr()) + bool has_plus = plus && plus < item + len; + + assert(!has_plus || plus > item); + size_t key_len = has_plus ? (size_t) (plus - item) : len; + +#define STREQ(literal, s, len) \ + ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) + + if (STREQ("lctrl", item, key_len)) { + mod |= SC_MOD_LCTRL; + } else if (STREQ("rctrl", item, key_len)) { + mod |= SC_MOD_RCTRL; + } else if (STREQ("lalt", item, key_len)) { + mod |= SC_MOD_LALT; + } else if (STREQ("ralt", item, key_len)) { + mod |= SC_MOD_RALT; + } else if (STREQ("lsuper", item, key_len)) { + mod |= SC_MOD_LSUPER; + } else if (STREQ("rsuper", item, key_len)) { + mod |= SC_MOD_RSUPER; + } else { + LOGW("Unknown modifier key: %.*s", (int) key_len, item); + return 0; + } +#undef STREQ + + if (!has_plus) { + break; + } + + item = plus + 1; + assert(len >= key_len + 1); + len -= key_len + 1; + } + + return mod; +} + static bool -parse_record_format(const char *optarg, enum recorder_format *format) { +parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + unsigned count = 0; + unsigned current = 0; + + // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper" + + for (;;) { + char *comma = strchr(s, ','); + if (comma && count == SC_MAX_SHORTCUT_MODS - 1) { + assert(count < SC_MAX_SHORTCUT_MODS); + LOGW("Too many shortcut modifiers alternatives"); + return false; + } + + assert(!comma || comma > s); + size_t limit = comma ? (size_t) (comma - s) : strlen(s); + + unsigned mod = parse_shortcut_mods_item(s, limit); + if (!mod) { + LOGE("Invalid modifier keys: %.*s", (int) limit, s); + return false; + } + + mods->data[current++] = mod; + ++count; + + if (!comma) { + break; + } + + s = comma + 1; + } + + mods->count = count; + + return true; +} + +#ifdef SC_TEST +// expose the function to unit-tests +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + return parse_shortcut_mods(s, mods); +} +#endif + +static bool +parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { - *format = RECORDER_FORMAT_MP4; + *format = SC_RECORD_FORMAT_MP4; return true; } if (!strcmp(optarg, "mkv")) { - *format = RECORDER_FORMAT_MKV; + *format = SC_RECORD_FORMAT_MKV; return true; } LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); return false; } -static enum recorder_format +static enum sc_record_format guess_record_format(const char *filename) { size_t len = strlen(filename); if (len < 4) { @@ -498,10 +615,10 @@ guess_record_format(const char *filename) { } const char *ext = &filename[len - 4]; if (!strcmp(ext, ".mp4")) { - return RECORDER_FORMAT_MP4; + return SC_RECORD_FORMAT_MP4; } if (!strcmp(ext, ".mkv")) { - return RECORDER_FORMAT_MKV; + return SC_RECORD_FORMAT_MKV; } return 0; } @@ -526,6 +643,9 @@ guess_record_format(const char *filename) { #define OPT_NO_MIPMAPS 1017 #define OPT_CODEC_OPTIONS 1018 #define OPT_FORCE_ADB_FORWARD 1019 +#define OPT_DISABLE_SCREENSAVER 1020 +#define OPT_SHORTCUT_MOD 1021 +#define OPT_NO_KEY_REPEAT 1022 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -534,6 +654,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"bit-rate", required_argument, NULL, 'b'}, {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"crop", required_argument, NULL, OPT_CROP}, + {"disable-screensaver", no_argument, NULL, + OPT_DISABLE_SCREENSAVER}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"force-adb-forward", no_argument, NULL, OPT_FORCE_ADB_FORWARD}, @@ -546,6 +668,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"no-control", no_argument, NULL, 'n'}, {"no-display", no_argument, NULL, 'N'}, {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, + {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT}, {"port", required_argument, NULL, 'p'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, @@ -556,6 +679,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_RENDER_EXPIRED_FRAMES}, {"rotation", required_argument, NULL, OPT_ROTATION}, {"serial", required_argument, NULL, 's'}, + {"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD}, {"show-touches", no_argument, NULL, 't'}, {"stay-awake", no_argument, NULL, 'w'}, {"turn-screen-off", no_argument, NULL, 'S'}, @@ -710,12 +834,23 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_NO_MIPMAPS: opts->mipmaps = false; break; + case OPT_NO_KEY_REPEAT: + opts->forward_key_repeat = false; + break; case OPT_CODEC_OPTIONS: opts->codec_options = optarg; break; case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; break; + case OPT_DISABLE_SCREENSAVER: + opts->disable_screensaver = true; + break; + case OPT_SHORTCUT_MOD: + if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/cli.h b/app/src/cli.h index 2e2bfe93..73dfe228 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -18,4 +18,9 @@ scrcpy_print_usage(const char *arg0); bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); +#ifdef SC_TEST +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); +#endif + #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index c5778c02..27c7903d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) { static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { size_t len = utf8_truncation_index(utf8, max_len); - buffer_write16be(buf, (uint16_t) len); - memcpy(&buf[2], utf8, len); - return 2 + len; + buffer_write32be(buf, len); + memcpy(&buf[4], utf8, len); + return 4 + len; } static uint16_t @@ -42,8 +42,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: buf[1] = msg->inject_keycode.action; buffer_write32be(&buf[2], msg->inject_keycode.keycode); - buffer_write32be(&buf[6], msg->inject_keycode.metastate); - return 10; + buffer_write32be(&buf[6], msg->inject_keycode.repeat); + buffer_write32be(&buf[10], msg->inject_keycode.metastate); + return 14; case CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(msg->inject_text.text, diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 0e85c97e..e0b480de 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,10 +10,11 @@ #include "android/keycodes.h" #include "common.h" +#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k + #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092 -#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ - (4 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) +// type: 1 byte; paste flag: 1 byte; length: 4 bytes +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) #define POINTER_ID_MOUSE UINT64_C(-1); @@ -43,6 +44,7 @@ struct control_msg { struct { enum android_keyevent_action action; enum android_keycode keycode; + uint32_t repeat; enum android_metastate metastate; } inject_keycode; struct { @@ -70,7 +72,7 @@ struct control_msg { }; }; -// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE +// buf size must be at least CONTROL_MSG_MAX_SIZE // return the number of bytes written size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf); diff --git a/app/src/controller.c b/app/src/controller.c index d59a7411..c5897e5d 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller, static bool process_msg(struct controller *controller, const struct control_msg *msg) { - unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; int length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; diff --git a/app/src/device_msg.c b/app/src/device_msg.c index db176129..09e68936 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -9,7 +9,7 @@ ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, struct device_msg *msg) { - if (len < 3) { + if (len < 5) { // at least type + empty string length return 0; // not available } @@ -17,8 +17,8 @@ device_msg_deserialize(const unsigned char *buf, size_t len, msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - uint16_t clipboard_len = buffer_read16be(&buf[1]); - if (clipboard_len > len - 3) { + size_t clipboard_len = buffer_read32be(&buf[1]); + if (clipboard_len > len - 5) { return 0; // not available } char *text = SDL_malloc(clipboard_len + 1); @@ -27,12 +27,12 @@ device_msg_deserialize(const unsigned char *buf, size_t len, return -1; } if (clipboard_len) { - memcpy(text, &buf[3], clipboard_len); + memcpy(text, &buf[5], clipboard_len); } text[clipboard_len] = '\0'; msg->clipboard.text = text; - return 3 + clipboard_len; + return 5 + clipboard_len; } default: LOGW("Unknown device message type: %d", (int) msg->type); diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 04723597..4b681e2c 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -7,8 +7,9 @@ #include "config.h" -#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 -#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) +#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k +// type: 1 byte; length: 4 bytes +#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 1054dcf9..ab48898d 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -92,6 +92,10 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); + MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT); + MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT); + MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); + MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); } if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { @@ -111,8 +115,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, } } - if (prefer_text) { - // do not forward alpha and space key events + if (prefer_text && !(mod & KMOD_CTRL)) { + // do not forward alpha and space key events (unless Ctrl is pressed) return false; } diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e8ba9f79..1d73980c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,6 +1,7 @@ #include "input_manager.h" #include +#include #include "config.h" #include "event_converter.h" @@ -10,6 +11,67 @@ static const int ACTION_DOWN = 1; static const int ACTION_UP = 1 << 1; +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +static inline uint16_t +to_sdl_mod(unsigned mod) { + uint16_t sdl_mod = 0; + if (mod & SC_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (mod & SC_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (mod & SC_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (mod & SC_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (mod & SC_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (mod & SC_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static bool +is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { + // keep only the relevant modifier keys + sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; + + assert(im->sdl_shortcut_mods.count); + assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS); + for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) { + if (im->sdl_shortcut_mods.data[i] == sdl_mod) { + return true; + } + } + + return false; +} + +void +input_manager_init(struct input_manager *im, + const struct scrcpy_options *options) +{ + im->control = options->control; + im->forward_key_repeat = options->forward_key_repeat; + im->prefer_text = options->prefer_text; + + const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; + assert(shortcut_mods->count); + assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); + for (unsigned i = 0; i < shortcut_mods->count; ++i) { + uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]); + assert(sdl_mod); + im->sdl_shortcut_mods.data[i] = sdl_mod; + } + im->sdl_shortcut_mods.count = shortcut_mods->count; +} + static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) { @@ -18,6 +80,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode, msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; + msg.inject_keycode.repeat = 0; if (actions & ACTION_DOWN) { msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; @@ -70,6 +133,16 @@ action_menu(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); } +static inline void +action_copy(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_COPY, actions, "COPY"); +} + +static inline void +action_cut(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_CUT, actions, "CUT"); +} + // turn the screen on if it was off, press BACK otherwise static void press_back_or_turn_screen_on(struct controller *controller) { @@ -101,16 +174,6 @@ collapse_notification_panel(struct controller *controller) { } } -static void -request_device_clipboard(struct controller *controller) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; - - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request device clipboard"); - } -} - static void set_device_clipboard(struct controller *controller, bool paste) { char *text = SDL_GetClipboardText(); @@ -210,6 +273,10 @@ rotate_client_right(struct screen *screen) { void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { + if (is_shortcut_mod(im, SDL_GetModState())) { + // A shortcut must never generate text events + return; + } if (!im->prefer_text) { char c = event->text[0]; if (isalpha(c) || c == ' ') { @@ -234,7 +301,7 @@ input_manager_process_text_input(struct input_manager *im, static bool convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, - bool prefer_text) { + bool prefer_text, uint32_t repeat) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -247,6 +314,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, return false; } + to->inject_keycode.repeat = repeat; to->inject_keycode.metastate = convert_meta_state(mod); return true; @@ -254,74 +322,52 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, void input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event, - bool control) { + const SDL_KeyboardEvent *event) { // control: indicates the state of the command-line option --no-control - // ctrl: the Ctrl key + bool control = im->control; - bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); - bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); - bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); - - // use Cmd on macOS, Ctrl on other platforms -#ifdef __APPLE__ - bool cmd = !ctrl && meta; -#else - if (meta) { - // no shortcuts involve Meta on platforms other than macOS, and it must - // not be forwarded to the device - return; - } - bool cmd = ctrl; // && !meta, already guaranteed -#endif - - if (alt) { - // no shortcuts involve Alt, and it must not be forwarded to the device - return; - } + bool smod = is_shortcut_mod(im, event->keysym.mod); struct controller *controller = im->controller; - // capture all Ctrl events - if (ctrl || cmd) { - SDL_Keycode keycode = event->keysym.sym; - bool down = event->type == SDL_KEYDOWN; + SDL_Keycode keycode = event->keysym.sym; + bool down = event->type == SDL_KEYDOWN; + bool ctrl = event->keysym.mod & KMOD_CTRL; + bool shift = event->keysym.mod & KMOD_SHIFT; + bool repeat = event->repeat; + + // The shortcut modifier is pressed + if (smod) { int action = down ? ACTION_DOWN : ACTION_UP; - bool repeat = event->repeat; - bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: - // Ctrl+h on all platform, since Cmd+h is already captured by - // the system on macOS to hide the window - if (control && ctrl && !meta && !shift && !repeat) { + if (control && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && cmd && !shift && !repeat) { + if (control && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && cmd && !shift && !repeat) { + if (control && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: - // Ctrl+m on all platform, since Cmd+m is already captured by - // the system on macOS to minimize the window - if (control && ctrl && !meta && !shift && !repeat) { + if (control && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: - if (control && cmd && !shift && !repeat) { + if (control && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && cmd && down) { + if (control && !repeat && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; @@ -329,67 +375,72 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_DOWN: - if (control && cmd && !shift) { + if (control && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (control && cmd && !shift) { + if (control && !shift) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (cmd && !shift && down) { + if (!shift && !repeat && down) { rotate_client_left(im->screen); } return; case SDLK_RIGHT: - if (cmd && !shift && down) { + if (!shift && !repeat && down) { rotate_client_right(im->screen); } return; case SDLK_c: - if (control && cmd && !shift && !repeat && down) { - request_device_clipboard(controller); + if (control && !shift && !repeat) { + action_copy(controller, action); + } + return; + case SDLK_x: + if (control && !shift && !repeat) { + action_cut(controller, action); } return; case SDLK_v: - if (control && cmd && !repeat && down) { + if (control && !repeat && down) { if (shift) { - // store the text in the device clipboard and paste - set_device_clipboard(controller, true); - } else { // inject the text as input events clipboard_paste(controller); + } else { + // store the text in the device clipboard and paste + set_device_clipboard(controller, true); } } return; case SDLK_f: - if (!shift && cmd && !repeat && down) { + if (!shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; - case SDLK_x: - if (!shift && cmd && !repeat && down) { + case SDLK_w: + if (!shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (!shift && cmd && !repeat && down) { + if (!shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (!shift && cmd && !repeat && down) { + if (!shift && !repeat && down) { struct fps_counter *fps_counter = im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; case SDLK_n: - if (control && cmd && !repeat && down) { + if (control && !repeat && down) { if (shift) { collapse_notification_panel(controller); } else { @@ -398,7 +449,7 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_r: - if (control && cmd && !shift && !repeat && down) { + if (control && !shift && !repeat && down) { rotate_device(controller); } return; @@ -411,8 +462,23 @@ input_manager_process_key(struct input_manager *im, return; } + if (event->repeat) { + if (!im->forward_key_repeat) { + return; + } + ++im->repeat; + } else { + im->repeat = 0; + } + + if (ctrl && !shift && keycode == SDLK_v && down && !repeat) { + // Synchronize the computer clipboard to the device clipboard before + // sending Ctrl+v, to allow seamless copy-paste. + set_device_clipboard(controller, false); + } + struct control_msg msg; - if (convert_input_key(event, &msg, im->prefer_text)) { + if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } @@ -427,7 +493,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = - screen_convert_to_frame_coords(screen, from->x, from->y); + screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); @@ -465,15 +531,15 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.position.screen_size = screen->frame_size; - int ww; - int wh; - SDL_GL_GetDrawableSize(screen->window, &ww, &wh); + int dw; + int dh; + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); // SDL touch event coordinates are normalized in the range [0; 1] - int32_t x = from->x * ww; - int32_t y = from->y * wh; + int32_t x = from->x * dw; + int32_t y = from->y * dh; to->inject_touch_event.position.point = - screen_convert_to_frame_coords(screen, x, y); + screen_convert_drawable_to_frame_coords(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; @@ -503,8 +569,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = - screen_convert_to_frame_coords(screen, from->x, from->y); - to->inject_touch_event.pressure = 1.f; + screen_convert_window_to_frame_coords(screen, from->x, from->y); + to->inject_touch_event.pressure = + from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); @@ -513,8 +580,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, void input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event, - bool control) { + const SDL_MouseButtonEvent *event) { + bool control = im->control; + if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; @@ -568,7 +636,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, struct position position = { .screen_size = screen->frame_size, - .point = screen_convert_to_frame_coords(screen, mouse_x, mouse_y), + .point = screen_convert_window_to_frame_coords(screen, + mouse_x, mouse_y), }; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 43fc0eeb..8811c457 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -3,28 +3,46 @@ #include +#include + #include "config.h" #include "common.h" #include "controller.h" #include "fps_counter.h" -#include "video_buffer.h" +#include "scrcpy.h" #include "screen.h" +#include "video_buffer.h" struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; + + // SDL reports repeated events as a boolean, but Android expects the actual + // number of repetitions. This variable keeps track of the count. + unsigned repeat; + + bool control; + bool forward_key_repeat; bool prefer_text; + + struct { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; + } sdl_shortcut_mods; }; +void +input_manager_init(struct input_manager *im, + const struct scrcpy_options *options); + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); void input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event, - bool control); + const SDL_KeyboardEvent *event); void input_manager_process_mouse_motion(struct input_manager *im, @@ -36,8 +54,7 @@ input_manager_process_touch(struct input_manager *im, void input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event, - bool control); + const SDL_MouseButtonEvent *event); void input_manager_process_mouse_wheel(struct input_manager *im, diff --git a/app/src/main.c b/app/src/main.c index 85e578ae..202c217c 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,5 +1,6 @@ #include "scrcpy.h" +#include #include #include #include @@ -42,7 +43,7 @@ convert_log_level_to_sdl(enum sc_log_level level) { return SDL_LOG_PRIORITY_ERROR; default: assert(!"unexpected log level"); - return SC_LOG_LEVEL_INFO; + return SDL_LOG_PRIORITY_INFO; } } @@ -71,7 +72,7 @@ main(int argc, char *argv[]) { } SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level); - SDL_LogSetAllPriority(sdl_log); + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); if (args.help) { scrcpy_print_usage(argv[0]); diff --git a/app/src/receiver.c b/app/src/receiver.c index 0474ff55..307eb5d5 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -25,10 +25,19 @@ receiver_destroy(struct receiver *receiver) { static void process_msg(struct device_msg *msg) { switch (msg->type) { - case DEVICE_MSG_TYPE_CLIPBOARD: + case DEVICE_MSG_TYPE_CLIPBOARD: { + char *current = SDL_GetClipboardText(); + bool same = current && !strcmp(current, msg->clipboard.text); + SDL_free(current); + if (same) { + LOGD("Computer clipboard unchanged"); + return; + } + LOGI("Device clipboard copied"); SDL_SetClipboardText(msg->clipboard.text); break; + } } } @@ -60,28 +69,29 @@ static int run_receiver(void *data) { struct receiver *receiver = data; - unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; + static unsigned char buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { - assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); - ssize_t r = net_recv(receiver->control_socket, buf, - DEVICE_MSG_SERIALIZED_MAX_SIZE - head); + assert(head < DEVICE_MSG_MAX_SIZE); + ssize_t r = net_recv(receiver->control_socket, buf + head, + DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); break; } - ssize_t consumed = process_msgs(buf, r); + head += r; + ssize_t consumed = process_msgs(buf, head); if (consumed == -1) { // an error occurred break; } if (consumed) { + head -= consumed; // shift the remaining data in the buffer - memmove(buf, &buf[consumed], r - consumed); - head = r - consumed; + memmove(buf, &buf[consumed], head); } } diff --git a/app/src/recorder.c b/app/src/recorder.c index 465b24e8..76edbd03 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -63,7 +63,7 @@ recorder_queue_clear(struct recorder_queue *queue) { bool recorder_init(struct recorder *recorder, const char *filename, - enum recorder_format format, + enum sc_record_format format, struct size declared_frame_size) { recorder->filename = SDL_strdup(filename); if (!recorder->filename) { @@ -105,10 +105,10 @@ recorder_destroy(struct recorder *recorder) { } static const char * -recorder_get_format_name(enum recorder_format format) { +recorder_get_format_name(enum sc_record_format format) { switch (format) { - case RECORDER_FORMAT_MP4: return "mp4"; - case RECORDER_FORMAT_MKV: return "matroska"; + case SC_RECORD_FORMAT_MP4: return "mp4"; + case SC_RECORD_FORMAT_MKV: return "matroska"; default: return NULL; } } diff --git a/app/src/recorder.h b/app/src/recorder.h index 4f5d526c..bc87a23b 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -8,14 +8,9 @@ #include "config.h" #include "common.h" +#include "scrcpy.h" #include "util/queue.h" -enum recorder_format { - RECORDER_FORMAT_AUTO, - RECORDER_FORMAT_MP4, - RECORDER_FORMAT_MKV, -}; - struct record_packet { AVPacket packet; struct record_packet *next; @@ -25,7 +20,7 @@ struct recorder_queue QUEUE(struct record_packet); struct recorder { char *filename; - enum recorder_format format; + enum sc_record_format format; AVFormatContext *ctx; struct size declared_frame_size; bool header_written; @@ -46,7 +41,7 @@ struct recorder { bool recorder_init(struct recorder *recorder, const char *filename, - enum recorder_format format, struct size declared_frame_size); + enum sc_record_format format, struct size declared_frame_size); void recorder_destroy(struct recorder *recorder); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 67ebf8c0..45068cbb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -8,6 +8,8 @@ #include #ifdef _WIN32 +// not needed here, but winsock2.h must never be included AFTER windows.h +# include # include #endif @@ -46,7 +48,14 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, - .prefer_text = false, // initialized later + .repeat = 0, + + // initialized later + .prefer_text = false, + .sdl_shortcut_mods = { + .data = {0}, + .count = 0, + }, }; #ifdef _WIN32 @@ -63,7 +72,8 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { // init SDL and set appropriate hints static bool -sdl_init_and_configure(bool display, const char *render_driver) { +sdl_init_and_configure(bool display, const char *render_driver, + bool disable_screensaver) { uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); @@ -112,8 +122,13 @@ sdl_init_and_configure(bool display, const char *render_driver) { LOGW("Could not disable minimize on focus loss"); } - // Do not disable the screensaver when scrcpy is running - SDL_EnableScreenSaver(); + if (disable_screensaver) { + LOGD("Screensaver disabled"); + SDL_DisableScreenSaver(); + } else { + LOGD("Screensaver enabled"); + SDL_EnableScreenSaver(); + } return true; } @@ -155,7 +170,7 @@ enum event_result { }; static enum event_result -handle_event(SDL_Event *event, bool control) { +handle_event(SDL_Event *event, const struct scrcpy_options *options) { switch (event->type) { case EVENT_STREAM_STOPPED: LOGD("Video stream stopped"); @@ -177,7 +192,7 @@ handle_event(SDL_Event *event, bool control) { screen_handle_window_event(&screen, &event->window); break; case SDL_TEXTINPUT: - if (!control) { + if (!options->control) { break; } input_manager_process_text_input(&input_manager, &event->text); @@ -186,16 +201,16 @@ handle_event(SDL_Event *event, bool control) { case SDL_KEYUP: // some key events do not interact with the device, so process the // event even if control is disabled - input_manager_process_key(&input_manager, &event->key, control); + input_manager_process_key(&input_manager, &event->key); break; case SDL_MOUSEMOTION: - if (!control) { + if (!options->control) { break; } input_manager_process_mouse_motion(&input_manager, &event->motion); break; case SDL_MOUSEWHEEL: - if (!control) { + if (!options->control) { break; } input_manager_process_mouse_wheel(&input_manager, &event->wheel); @@ -204,8 +219,7 @@ handle_event(SDL_Event *event, bool control) { case SDL_MOUSEBUTTONUP: // some mouse events do not interact with the device, so process // the event even if control is disabled - input_manager_process_mouse_button(&input_manager, &event->button, - control); + input_manager_process_mouse_button(&input_manager, &event->button); break; case SDL_FINGERMOTION: case SDL_FINGERDOWN: @@ -213,7 +227,7 @@ handle_event(SDL_Event *event, bool control) { input_manager_process_touch(&input_manager, &event->tfinger); break; case SDL_DROPFILE: { - if (!control) { + if (!options->control) { break; } file_handler_action_t action; @@ -230,16 +244,15 @@ handle_event(SDL_Event *event, bool control) { } static bool -event_loop(bool display, bool control) { - (void) display; +event_loop(const struct scrcpy_options *options) { #ifdef CONTINUOUS_RESIZING_WORKAROUND - if (display) { + if (options->display) { SDL_AddEventWatch(event_watcher, NULL); } #endif SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(&event, control); + enum event_result result = handle_event(&event, options); switch (result) { case EVENT_RESULT_STOPPED_BY_USER: return true; @@ -321,7 +334,8 @@ scrcpy(const struct scrcpy_options *options) { bool controller_initialized = false; bool controller_started = false; - if (!sdl_init_and_configure(options->display, options->render_driver)) { + if (!sdl_init_and_configure(options->display, options->render_driver, + options->disable_screensaver)) { goto end; } @@ -427,9 +441,9 @@ scrcpy(const struct scrcpy_options *options) { } } - input_manager.prefer_text = options->prefer_text; + input_manager_init(&input_manager, options); - ret = event_loop(options->display, options->control); + ret = event_loop(options); LOGD("quit..."); screen_destroy(&screen); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 70d99433..86a2b57b 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -2,13 +2,46 @@ #define SCRCPY_H #include +#include #include #include "config.h" -#include "common.h" -#include "input_manager.h" -#include "recorder.h" -#include "util/log.h" + +enum sc_log_level { + SC_LOG_LEVEL_DEBUG, + SC_LOG_LEVEL_INFO, + SC_LOG_LEVEL_WARN, + SC_LOG_LEVEL_ERROR, +}; + +enum sc_record_format { + SC_RECORD_FORMAT_AUTO, + SC_RECORD_FORMAT_MP4, + SC_RECORD_FORMAT_MKV, +}; + +#define SC_MAX_SHORTCUT_MODS 8 + +enum sc_shortcut_mod { + SC_MOD_LCTRL = 1 << 0, + SC_MOD_RCTRL = 1 << 1, + SC_MOD_LALT = 1 << 2, + SC_MOD_RALT = 1 << 3, + SC_MOD_LSUPER = 1 << 4, + SC_MOD_RSUPER = 1 << 5, +}; + +struct sc_shortcut_mods { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; +}; + +struct sc_port_range { + uint16_t first; + uint16_t last; +}; + +#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) struct scrcpy_options { const char *serial; @@ -19,15 +52,16 @@ struct scrcpy_options { const char *render_driver; const char *codec_options; enum sc_log_level log_level; - enum recorder_format record_format; - struct port_range port_range; + enum sc_record_format record_format; + struct sc_port_range port_range; + struct sc_shortcut_mods shortcut_mods; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; int8_t lock_video_orientation; uint8_t rotation; - int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" - int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; uint16_t display_id; @@ -43,6 +77,8 @@ struct scrcpy_options { bool mipmaps; bool stay_awake; bool force_adb_forward; + bool disable_screensaver; + bool forward_key_repeat; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -54,18 +90,22 @@ struct scrcpy_options { .render_driver = NULL, \ .codec_options = NULL, \ .log_level = SC_LOG_LEVEL_INFO, \ - .record_format = RECORDER_FORMAT_AUTO, \ + .record_format = SC_RECORD_FORMAT_AUTO, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ }, \ + .shortcut_mods = { \ + .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ + .count = 2, \ + }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .rotation = 0, \ - .window_x = WINDOW_POSITION_UNDEFINED, \ - .window_y = WINDOW_POSITION_UNDEFINED, \ + .window_x = SC_WINDOW_POSITION_UNDEFINED, \ + .window_y = SC_WINDOW_POSITION_UNDEFINED, \ .window_width = 0, \ .window_height = 0, \ .display_id = 0, \ @@ -81,6 +121,8 @@ struct scrcpy_options { .mipmaps = true, \ .stay_awake = false, \ .force_adb_forward = false, \ + .disable_screensaver = false, \ + .forward_key_repeat = true, \ } bool diff --git a/app/src/screen.c b/app/src/screen.c index 66a1163b..fe2bc867 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -8,6 +8,7 @@ #include "common.h" #include "compat.h" #include "icon.xpm" +#include "scrcpy.h" #include "tiny_xpm.h" #include "video_buffer.h" #include "util/lock.h" @@ -257,9 +258,9 @@ screen_init_rendering(struct screen *screen, const char *window_title, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != WINDOW_POSITION_UNDEFINED + int x = window_x != SC_WINDOW_POSITION_UNDEFINED ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != WINDOW_POSITION_UNDEFINED + int y = window_y != SC_WINDOW_POSITION_UNDEFINED ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, @@ -579,14 +580,14 @@ screen_handle_window_event(struct screen *screen, } struct point -screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { +screen_convert_drawable_to_frame_coords(struct screen *screen, + int32_t x, int32_t y) { unsigned rotation = screen->rotation; assert(rotation < 4); int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; - screen_hidpi_scale_coords(screen, &x, &y); x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; @@ -615,6 +616,13 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { return result; } +struct point +screen_convert_window_to_frame_coords(struct screen *screen, + int32_t x, int32_t y) { + screen_hidpi_scale_coords(screen, &x, &y); + return screen_convert_drawable_to_frame_coords(screen, x, y); +} + void screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { // take the HiDPI scaling (dw/ww and dh/wh) into account diff --git a/app/src/screen.h b/app/src/screen.h index aa6218f7..c4fbbf66 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -9,8 +9,6 @@ #include "common.h" #include "opengl.h" -#define WINDOW_POSITION_UNDEFINED (-0x8000) - struct video_buffer; struct screen { @@ -76,7 +74,7 @@ void screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) -// window_x and window_y accept WINDOW_POSITION_UNDEFINED +// window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED bool screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top, @@ -126,7 +124,14 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels struct point -screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); +screen_convert_window_to_frame_coords(struct screen *screen, + int32_t x, int32_t y); + +// convert point from drawable coordinates to frame coordinates +// x and y are expressed in pixels +struct point +screen_convert_drawable_to_frame_coords(struct screen *screen, + int32_t x, int32_t y); // Convert coordinates from window to drawable. // Events are expressed in window coordinates, but content is expressed in diff --git a/app/src/server.c b/app/src/server.c index fb498d63..05b2cf91 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -143,7 +143,7 @@ listen_on_port(uint16_t port) { static bool enable_tunnel_reverse_any_port(struct server *server, - struct port_range port_range) { + struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { if (!enable_tunnel_reverse(server->serial, port)) { @@ -189,7 +189,7 @@ enable_tunnel_reverse_any_port(struct server *server, static bool enable_tunnel_forward_any_port(struct server *server, - struct port_range port_range) { + struct sc_port_range port_range) { server->tunnel_forward = true; uint16_t port = port_range.first; for (;;) { @@ -217,7 +217,7 @@ enable_tunnel_forward_any_port(struct server *server, } static bool -enable_tunnel_any_port(struct server *server, struct port_range port_range, +enable_tunnel_any_port(struct server *server, struct sc_port_range port_range, bool force_adb_forward) { if (!force_adb_forward) { // Attempt to use "adb reverse" diff --git a/app/src/server.h b/app/src/server.h index 2215d817..254afe30 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -9,6 +9,7 @@ #include "config.h" #include "command.h" #include "common.h" +#include "scrcpy.h" #include "util/log.h" #include "util/net.h" @@ -20,7 +21,7 @@ struct server { socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; - struct port_range port_range; + struct sc_port_range port_range; uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" @@ -47,7 +48,7 @@ struct server_params { enum sc_log_level log_level; const char *crop; const char *codec_options; - struct port_range port_range; + struct sc_port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; diff --git a/app/src/util/log.h b/app/src/util/log.h index 8c5c7dee..5955c7fb 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -3,13 +3,6 @@ #include -enum sc_log_level { - SC_LOG_LEVEL_DEBUG, - SC_LOG_LEVEL_INFO, - SC_LOG_LEVEL_WARN, - SC_LOG_LEVEL_ERROR, -}; - #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c index ba3f9f06..d61e6918 100644 --- a/app/tests/test_buffer_util.c +++ b/app/tests/test_buffer_util.c @@ -65,7 +65,10 @@ static void test_buffer_read64be(void) { assert(val == 0xABCD1234567890EF); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_buffer_write16be(); test_buffer_write32be(); test_buffer_write64be(); diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c index dbe50aab..f8beb880 100644 --- a/app/tests/test_cbuf.c +++ b/app/tests/test_cbuf.c @@ -65,7 +65,10 @@ static void test_cbuf_push_take(void) { assert(item == 35); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_cbuf_empty(); test_cbuf_full(); test_cbuf_push_take(); diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index c5d95633..1024dba6 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -1,7 +1,9 @@ #include +#include #include "cli.h" #include "common.h" +#include "scrcpy.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { @@ -73,7 +75,6 @@ static void test_options(void) { const struct scrcpy_options *opts = &args.opts; assert(opts->always_on_top); - fprintf(stderr, "%d\n", (int) opts->bit_rate); assert(opts->bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); @@ -84,7 +85,7 @@ static void test_options(void) { assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); - assert(opts->record_format == RECORDER_FORMAT_MKV); + assert(opts->record_format == SC_RECORD_FORMAT_MKV); assert(opts->render_expired_frames); assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); @@ -119,13 +120,54 @@ static void test_options2(void) { assert(!opts->control); assert(!opts->display); assert(!strcmp(opts->record_filename, "file.mp4")); - assert(opts->record_format == RECORDER_FORMAT_MP4); + assert(opts->record_format == SC_RECORD_FORMAT_MP4); } -int main(void) { +static void test_parse_shortcut_mods(void) { + struct sc_shortcut_mods mods; + bool ok; + + ok = sc_parse_shortcut_mods("lctrl", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == SC_MOD_LCTRL); + + ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT)); + + ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); + assert(ok); + assert(mods.count == 2); + assert(mods.data[0] == SC_MOD_RCTRL); + assert(mods.data[1] == SC_MOD_LALT); + + ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); + assert(ok); + assert(mods.count == 3); + assert(mods.data[0] == SC_MOD_LSUPER); + assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT)); + assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT)); + + ok = sc_parse_shortcut_mods("", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl+", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl,", &mods); + assert(!ok); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_flag_version(); test_flag_help(); test_options(); test_options2(); + test_parse_shortcut_mods(); return 0; }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c6ff7b2e..b58c8e20 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -9,18 +9,20 @@ static void test_serialize_inject_keycode(void) { .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, + .repeat = 5, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 10); + assert(size == 14); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER + 0x00, 0x00, 0x00, 0X05, // repeat 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -34,13 +36,13 @@ static void test_serialize_inject_text(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 18); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_TEXT, - 0x00, 0x0d, // text length + 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -54,15 +56,17 @@ static void test_serialize_inject_text_long(void) { text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; + unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; - expected[1] = 0x01; - expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + expected[1] = 0x00; + expected[2] = 0x00; + expected[3] = 0x01; + expected[4] = 0x2c; // text length (32 bits) + memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } @@ -88,7 +92,7 @@ static void test_serialize_inject_touch_event(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 28); @@ -123,7 +127,7 @@ static void test_serialize_inject_scroll_event(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 21); @@ -142,7 +146,7 @@ static void test_serialize_back_or_screen_on(void) { .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -157,7 +161,7 @@ static void test_serialize_expand_notification_panel(void) { .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -172,7 +176,7 @@ static void test_serialize_collapse_notification_panel(void) { .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -187,7 +191,7 @@ static void test_serialize_get_clipboard(void) { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -206,14 +210,14 @@ static void test_serialize_set_clipboard(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 17); + assert(size == 19); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_CLIPBOARD, 1, // paste - 0x00, 0x0d, // text length + 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -227,7 +231,7 @@ static void test_serialize_set_screen_power_mode(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 2); @@ -243,7 +247,7 @@ static void test_serialize_rotate_device(void) { .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + unsigned char buf[CONTROL_MSG_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); assert(size == 1); @@ -253,7 +257,10 @@ static void test_serialize_rotate_device(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index e163ad72..3dfd0b0f 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -4,16 +4,17 @@ #include "device_msg.h" #include + static void test_deserialize_clipboard(void) { const unsigned char input[] = { DEVICE_MSG_TYPE_CLIPBOARD, - 0x00, 0x03, // text length + 0x00, 0x00, 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" }; struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); - assert(r == 6); + assert(r == 8); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); @@ -22,7 +23,33 @@ static void test_deserialize_clipboard(void) { device_msg_destroy(&msg); } -int main(void) { +static void test_deserialize_clipboard_big(void) { + unsigned char input[DEVICE_MSG_MAX_SIZE]; + input[0] = DEVICE_MSG_TYPE_CLIPBOARD; + input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; + input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; + input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8; + input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu; + + memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); + + struct device_msg msg; + ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + assert(r == DEVICE_MSG_MAX_SIZE); + + assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); + assert(msg.clipboard.text); + assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); + assert(msg.clipboard.text[0] == 'a'); + + device_msg_destroy(&msg); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_deserialize_clipboard(); + test_deserialize_clipboard_big(); return 0; } diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index b0950bb0..e10821cd 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -32,7 +32,10 @@ static void test_queue(void) { assert(queue_is_empty(&queue)); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_queue(); return 0; } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index a88bca0e..7b9c61da 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -286,7 +286,10 @@ static void test_parse_integer_with_suffix(void) { assert(!ok); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_xstrncpy_simple(); test_xstrncpy_just_fit(); test_xstrncpy_truncated(); diff --git a/cross_win32.txt b/cross_win32.txt index 7b420690..bef2e5d5 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index cb9e0954..5a348738 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' diff --git a/meson.build b/meson.build index 46b9a092..8492f520 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.14', + version: '1.15.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index c6cbcf7b..9cccd0bd 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ - ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ - ffmpeg-4.2.2-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.3.1-win32-shared.zip \ + 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ + ffmpeg-4.3.1-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ - 8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ - ffmpeg-4.2.2-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.3.1-win32-dev.zip \ + 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ + ffmpeg-4.3.1-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ - 5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ - ffmpeg-4.2.2-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.3.1-win64-shared.zip \ + dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ + ffmpeg-4.3.1-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ - f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ - ffmpeg-4.2.2-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.3.1-win64-dev.zip \ + 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ + ffmpeg-4.3.1-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.12 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.0-windows.zip \ - 854305f9a702f5ea2c3de73edde402bd26afa0ee944c9b0c4380420f5a862e0d \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.4-windows.zip \ + 413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee \ platform-tools diff --git a/server/build.gradle b/server/build.gradle index c8ff85d6..c7c0abb0 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 16 - versionName "1.14" + versionCode 18 + versionName "1.15.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e3be2faf..cedbe186 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.14 +SCRCPY_VERSION_NAME=1.15.1 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 74555636..d0ea141b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,18 +19,19 @@ public final class CleanUp { // not instantiable } - public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { - boolean needProcess = disableShowTouches || restoreStayOn != -1; + public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { + boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode; if (needProcess) { - startProcess(disableShowTouches, restoreStayOn); + startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } - private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)}; + private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf( + restoreStayOn), String.valueOf(restoreNormalPowerMode)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -59,6 +60,7 @@ public final class CleanUp { boolean disableShowTouches = Boolean.parseBoolean(args[0]); int restoreStayOn = Integer.parseInt(args[1]); + boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]); if (disableShowTouches || restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); @@ -73,5 +75,10 @@ public final class CleanUp { } } } + + if (restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 7d0ab7a6..736acf80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,8 +17,6 @@ public final class ControlMessage { public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_ROTATE_DEVICE = 10; - public static final int FLAGS_PASTE = 1; - private int type; private String text; private int metaState; // KeyEvent.META_* @@ -30,16 +28,18 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; - private int flags; + private boolean paste; + private int repeat; private ControlMessage() { } - public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_KEYCODE; msg.action = action; msg.keycode = keycode; + msg.repeat = repeat; msg.metaState = metaState; return msg; } @@ -75,9 +75,7 @@ public final class ControlMessage { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; - if (paste) { - msg.flags = FLAGS_PASTE; - } + msg.paste = paste; return msg; } @@ -141,7 +139,11 @@ public final class ControlMessage { return vScroll; } - public int getFlags() { - return flags; + public boolean getPaste() { + return paste; + } + + public int getRepeat() { + return repeat; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index fbf49a61..ce185103 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,20 +8,19 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k + + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; - private static final int RAW_BUFFER_SIZE = 4096; - - private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; public ControlMessageReader() { // invariant: the buffer is always in "get" mode @@ -99,20 +98,23 @@ public class ControlMessageReader { } int action = toUnsigned(buffer.get()); int keycode = buffer.getInt(); + int repeat = buffer.getInt(); int metaState = buffer.getInt(); - return ControlMessage.createInjectKeycode(action, keycode, metaState); + return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } private String parseString() { - if (buffer.remaining() < 2) { + if (buffer.remaining() < 4) { return null; } - int len = toUnsigned(buffer.getShort()); + int len = buffer.getInt(); if (buffer.remaining() < len) { return null; } - buffer.get(textBuffer, 0, len); - return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + 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 ControlMessage parseInjectText() { @@ -152,12 +154,12 @@ public class ControlMessageReader { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; } - boolean parse = buffer.get() != 0; + boolean paste = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text, parse); + return ControlMessage.createSetClipboard(text, paste); } private ControlMessage parseSetScreenPowerMode() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 960c6a6e..2ad26a95 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -8,11 +8,16 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class Controller { private static final int DEVICE_ID_VIRTUAL = -1; + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; @@ -24,6 +29,8 @@ public class Controller { private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + private boolean keepPowerModeOff; + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; @@ -48,10 +55,10 @@ public class Controller { public void control() throws IOException { // on start, power on the device if (!device.isScreenOn()) { - device.injectKeycode(KeyEvent.KEYCODE_POWER); + device.injectKeycode(KeyEvent.KEYCODE_WAKEUP); // dirty hack - // After POWER is injected, the device is powered on asynchronously. + // After the keycode is injected, the device is powered on asynchronously. // To turn the device screen off while mirroring, the client will send a message that // would be handled before the device is actually powered on, so its effect would // be "canceled" once the device is turned back on. @@ -74,7 +81,7 @@ public class Controller { switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: if (device.supportsInputEvents()) { - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: @@ -110,14 +117,14 @@ public class Controller { } break; case ControlMessage.TYPE_SET_CLIPBOARD: - boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - setClipboard(msg.getText(), paste); + setClipboard(msg.getText(), msg.getPaste()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { int mode = msg.getAction(); - boolean setPowerModeOk = device.setScreenPowerMode(mode); + boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { + keepPowerModeOff = mode == Device.POWER_MODE_OFF; Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } } @@ -130,8 +137,11 @@ public class Controller { } } - private boolean injectKeycode(int action, int keycode, int metaState) { - return device.injectKeyEvent(action, keycode, 0, metaState); + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { + if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + schedulePowerModeOff(); + } + return device.injectKeyEvent(action, keycode, repeat, metaState); } private boolean injectChar(char c) { @@ -166,7 +176,7 @@ public class Controller { Point point = device.getPhysicalPoint(position); if (point == null) { - // ignore event + Ln.w("Ignore touch event, it was generated for a different device size"); return false; } @@ -224,8 +234,24 @@ public class Controller { return device.injectEvent(event); } + /** + * Schedule a call to set power mode to off after a small delay. + */ + private static void schedulePowerModeOff() { + EXECUTOR.schedule(new Runnable() { + @Override + public void run() { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); + } + }, 200, TimeUnit.MILLISECONDS); + } + private boolean pressBackOrTurnScreenOn() { - int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; + int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_WAKEUP; + if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_WAKEUP) { + schedulePowerModeOff(); + } return device.injectKeycode(keycode); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 349486c3..de551f35 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -80,23 +81,28 @@ public final class Device { if (options.getControl()) { // If control is enabled, synchronize Android clipboard to the computer automatically - serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { - @Override - public void dispatchPrimaryClipChanged() { - if (isSettingClipboard.get()) { - // This is a notification for the change we are currently applying, ignore it - return; - } - synchronized (Device.this) { - if (clipboardListener != null) { - String text = getClipboardText(); - if (text != null) { - clipboardListener.onClipboardTextChanged(text); + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } } } } - } - }); + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } } if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { @@ -199,7 +205,11 @@ public final class Device { } public String getClipboardText() { - CharSequence s = serviceManager.getClipboardManager().getText(); + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager == null) { + return null; + } + CharSequence s = clipboardManager.getText(); if (s == null) { return null; } @@ -207,16 +217,30 @@ public final class Device { } public boolean setClipboardText(String text) { + ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + if (clipboardManager == null) { + return false; + } + + String currentClipboard = getClipboardText(); + if (currentClipboard == null || currentClipboard.equals(text)) { + // The clipboard already contains the requested text. + // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause + // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this + // problem, do not explicitly set the clipboard text if it already contains the expected content. + return false; + } + isSettingClipboard.set(true); - boolean ok = serviceManager.getClipboardManager().setText(text); + boolean ok = clipboardManager.setText(text); isSettingClipboard.set(false); return ok; } /** - * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + * @param mode one of the {@code POWER_MODE_*} constants */ - public boolean setScreenPowerMode(int mode) { + public static boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index 6c7f3634..15d91a35 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -7,10 +7,10 @@ import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; - private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + 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[MAX_EVENT_SIZE]; + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { @@ -21,7 +21,7 @@ public class DeviceMessageWriter { String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putShort((short) len); + buffer.putInt(len); buffer.put(raw, 0, len); output.write(rawBuffer, 0, buffer.position()); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 44b3afd4..d257e319 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -49,7 +49,7 @@ public final class Server { } } - CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); + CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true); boolean tunnelForward = options.isTunnelForward(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index c4ce59c2..6f4b9c04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -77,7 +77,14 @@ public final class ServiceManager { public ClipboardManager getClipboardManager() { if (clipboardManager == null) { - clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + IInterface clipboard = getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { + // Some devices have no clipboard manager + // + // + return null; + } + clipboardManager = new ClipboardManager(clipboard); } return clipboardManager; } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f5fa4d09..5eb52760 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -25,6 +25,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -37,6 +38,7 @@ public class ControlMessageReaderTest { 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()); } @@ -48,7 +50,7 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -68,7 +70,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -218,7 +220,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -228,9 +230,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); - - boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - Assert.assertTrue(parse); + Assert.assertTrue(event.getPaste()); } @Test @@ -246,7 +246,7 @@ public class ControlMessageReaderTest { Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); - dos.writeShort(rawText.length); + dos.writeInt(rawText.length); dos.write(rawText); byte[] packet = bos.toByteArray(); @@ -256,9 +256,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(text, event.getText()); - - boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; - Assert.assertTrue(parse); + Assert.assertTrue(event.getPaste()); } @Test @@ -308,11 +306,13 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(0); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(1); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -322,12 +322,14 @@ public class ControlMessageReaderTest { 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(); 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()); } @@ -341,6 +343,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(4); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -353,6 +356,7 @@ public class ControlMessageReaderTest { 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(); @@ -360,6 +364,7 @@ public class ControlMessageReaderTest { 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)); @@ -369,6 +374,7 @@ public class ControlMessageReaderTest { 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()); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index df12f647..88bf2af9 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -19,7 +19,7 @@ public class DeviceMessageWriterTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); - dos.writeShort(data.length); + dos.writeInt(data.length); dos.write(data); byte[] expected = bos.toByteArray();