diff --git a/README.md b/README.md index e7af9b25..3611306d 100644 --- a/README.md +++ b/README.md @@ -243,20 +243,23 @@ To run without installing: ## Shortcuts - | Action | Shortcut | - | ------------------------------------- | -------------:| - | switch fullscreen mode | `Ctrl`+`f` | - | resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | - | resize window to remove black borders | `Ctrl`+`x` | - | click on `HOME` | `Ctrl`+`h` | - | click on `BACK` | `Ctrl`+`b` | - | click on `APP_SWITCH` | `Ctrl`+`m` | - | click on `VOLUME_UP` | `Ctrl`+`+` | - | click on `VOLUME_DOWN` | `Ctrl`+`-` | - | click on `POWER` | `Ctrl`+`p` | - | turn screen on | _Right-click_ | - | paste computer clipboard to device | `Ctrl`+`v` | - | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | + | Action | Shortcut | + | -------------------------------------- |:---------------------------- | + | switch fullscreen mode | `Ctrl`+`f` | + | resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | + | resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | + | click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | + | click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | + | click on `APP_SWITCH` | `Ctrl`+`m` | + | click on `VOLUME_UP` | `Ctrl`+`+` | + | click on `VOLUME_DOWN` | `Ctrl`+`-` | + | click on `POWER` | `Ctrl`+`p` | + | turn screen on | _Right-click¹_ | + | paste computer clipboard to device | `Ctrl`+`v` | + | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | + +_¹Double-click on black borders to remove them._ +_²Right-click turns the screen on if it was off, presses BACK otherwise._ ## Why _scrcpy_? diff --git a/app/src/command.c b/app/src/command.c index 6a5a38e4..246d1515 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -45,6 +45,13 @@ process_t adb_forward(const char *serial, uint16_t local_port, const char *devic return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } +process_t adb_forward_remove(const char *serial, uint16_t local_port) { + char local[4 + 5 + 1]; // tcp:PORT + sprintf(local, "tcp:%" PRIu16, local_port); + const char *const adb_cmd[] = {"forward", "--remove", local}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME diff --git a/app/src/command.h b/app/src/command.h index 5308aa6a..ed7d5faf 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -39,6 +39,7 @@ SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code); process_t adb_execute(const char *serial, const char *const adb_cmd[], int len); process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name); +process_t adb_forward_remove(const char *serial, uint16_t local_port); process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port); process_t adb_reverse_remove(const char *serial, const char *device_socket_name); process_t adb_push(const char *serial, const char *local, const char *remote); diff --git a/app/src/controlevent.c b/app/src/controlevent.c index d288cee4..ba52e717 100644 --- a/app/src/controlevent.c +++ b/app/src/controlevent.c @@ -39,9 +39,9 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu // injecting a text takes time, so limit the text length len = TEXT_MAX_LENGTH; } - buf[1] = (Uint8) len; - memcpy(&buf[2], event->text_event.text, len); - return 2 + len; + write16(&buf[1], (Uint16) len); + memcpy(&buf[3], event->text_event.text, len); + return 3 + len; } case CONTROL_EVENT_TYPE_MOUSE: buf[1] = event->mouse_event.action; diff --git a/app/src/controlevent.h b/app/src/controlevent.h index 7c52d6e0..471a9a9b 100644 --- a/app/src/controlevent.h +++ b/app/src/controlevent.h @@ -9,8 +9,8 @@ #include "common.h" #define CONTROL_EVENT_QUEUE_SIZE 64 -#define SERIALIZED_EVENT_MAX_SIZE 33 -#define TEXT_MAX_LENGTH 256 +#define TEXT_MAX_LENGTH 300 +#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH enum control_event_type { CONTROL_EVENT_TYPE_KEYCODE, @@ -20,7 +20,7 @@ enum control_event_type { CONTROL_EVENT_TYPE_COMMAND, }; -#define CONTROL_EVENT_COMMAND_SCREEN_ON 0 +#define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0 struct control_event { enum control_event_type type; diff --git a/app/src/inputmanager.c b/app/src/inputmanager.c index 81fa9619..4a1140d4 100644 --- a/app/src/inputmanager.c +++ b/app/src/inputmanager.c @@ -78,10 +78,11 @@ static inline void action_volume_down(struct controller *controller) { send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN"); } -static void turn_screen_on(struct controller *controller) { +// turn the screen on if it was off, press BACK otherwise +static void press_back_or_turn_screen_on(struct controller *controller) { struct control_event control_event; control_event.type = CONTROL_EVENT_TYPE_COMMAND; - control_event.command_event.action = CONTROL_EVENT_COMMAND_SCREEN_ON; + control_event.command_event.action = CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON; if (!controller_push_event(controller, &control_event)) { LOGW("Cannot turn screen on"); @@ -225,9 +226,25 @@ void input_manager_process_mouse_motion(struct input_manager *input_manager, void input_manager_process_mouse_button(struct input_manager *input_manager, const SDL_MouseButtonEvent *event) { - if (event->button == SDL_BUTTON_RIGHT && event->type == SDL_MOUSEBUTTONDOWN) { - turn_screen_on(input_manager->controller); - return; + if (event->type == SDL_MOUSEBUTTONDOWN) { + if (event->button == SDL_BUTTON_RIGHT) { + press_back_or_turn_screen_on(input_manager->controller); + return; + } + if (event->button == SDL_BUTTON_MIDDLE) { + action_home(input_manager->controller); + return; + } + // double-click on black borders resize to fit the device screen + if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { + SDL_bool outside_device_screen = + event->x < 0 || event->x >= input_manager->screen->frame_size.width || + event->y < 0 || event->y >= input_manager->screen->frame_size.height; + if (outside_device_screen) { + screen_resize_to_fit(input_manager->screen); + } + return; + } }; struct control_event control_event; if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) { diff --git a/app/src/main.c b/app/src/main.c index 01d9f175..60ce023e 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -57,14 +57,17 @@ static void usage(const char *arg0) { " resize window to 1:1 (pixel-perfect)\n" "\n" " Ctrl+x\n" + " Double-click on black borders\n" " resize window to remove black borders\n" "\n" " Ctrl+h\n" " Home\n" + " Middle-click\n" " click on HOME\n" "\n" " Ctrl+b\n" " Ctrl+Backspace\n" + " Right-click (when screen is on)\n" " click on BACK\n" "\n" " Ctrl+m\n" @@ -79,7 +82,7 @@ static void usage(const char *arg0) { " Ctrl+p\n" " click on POWER (turn screen on/off)\n" "\n" - " Right-click\n" + " Right-click (when screen is off)\n" " turn screen on\n" "\n" " Ctrl+v\n" diff --git a/app/src/net.c b/app/src/net.c index 60fb3988..1d68f068 100644 --- a/app/src/net.c +++ b/app/src/net.c @@ -18,6 +18,26 @@ typedef struct in_addr IN_ADDR; #endif +socket_t net_connect(Uint32 addr, Uint16 port) { + socket_t sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) { + perror("socket"); + return INVALID_SOCKET; + } + + SOCKADDR_IN sin; + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(addr); + sin.sin_port = htons(port); + + if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + perror("connect"); + return INVALID_SOCKET; + } + + return sock; +} + socket_t net_listen(Uint32 addr, Uint16 port, int backlog) { socket_t sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET) { diff --git a/app/src/net.h b/app/src/net.h index 03ad8c39..52a1e9a4 100644 --- a/app/src/net.h +++ b/app/src/net.h @@ -21,6 +21,7 @@ SDL_bool net_init(void); void net_cleanup(void); +socket_t net_connect(Uint32 addr, Uint16 port); socket_t net_listen(Uint32 addr, Uint16 port, int backlog); socket_t net_accept(socket_t server_socket); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8c549d67..4dd480b6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -35,7 +35,29 @@ static struct input_manager input_manager = { .screen = &screen, }; +#if defined(__APPLE__) || defined(__WINDOWS__) +# define CONTINUOUS_RESIZING_WORKAROUND +#endif + +#ifdef CONTINUOUS_RESIZING_WORKAROUND +// On Windows and MacOS, resizing blocks the event loop, so resizing events are +// not triggered. As a workaround, handle them in an event handler. +// +// +// +static int event_watcher(void *data, SDL_Event *event) { + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { + // called from another thread, not very safe, but it's a workaround! + screen_render(&screen); + } + return 0; +} +#endif + static void event_loop(void) { +#ifdef CONTINUOUS_RESIZING_WORKAROUND + SDL_AddEventWatch(event_watcher, NULL); +#endif SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { @@ -103,9 +125,9 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b // managed by the event loop. This blocking call blocks the event loop, so // timeout the connection not to block indefinitely in case of SIGTERM. #define SERVER_CONNECT_TIMEOUT_MS 2000 - socket_t device_socket = server_connect_to(&server, serial, SERVER_CONNECT_TIMEOUT_MS); + socket_t device_socket = server_connect_to(&server, SERVER_CONNECT_TIMEOUT_MS); if (device_socket == INVALID_SOCKET) { - server_stop(&server, serial); + server_stop(&server); ret = SDL_FALSE; goto finally_destroy_server; } @@ -117,13 +139,13 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b // therefore, we transmit the screen size before the video stream, to be able // to init the window immediately if (!device_read_info(device_socket, device_name, &frame_size)) { - server_stop(&server, serial); + server_stop(&server); ret = SDL_FALSE; goto finally_destroy_server; } if (!frames_init(&frames)) { - server_stop(&server, serial); + server_stop(&server); ret = SDL_FALSE; goto finally_destroy_server; } @@ -134,7 +156,7 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b // start the decoder if (!decoder_start(&decoder)) { ret = SDL_FALSE; - server_stop(&server, serial); + server_stop(&server); goto finally_destroy_frames; } @@ -165,7 +187,7 @@ finally_destroy_controller: finally_stop_decoder: decoder_stop(&decoder); // stop the server before decoder_join() to wake up the decoder - server_stop(&server, serial); + server_stop(&server); decoder_join(&decoder); finally_destroy_frames: frames_destroy(&frames); diff --git a/app/src/screen.c b/app/src/screen.c index c6838946..6587d659 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -18,8 +18,8 @@ SDL_bool sdl_init_and_configure(void) { atexit(SDL_Quit); - // Bilinear resizing - if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + // Use the best available scale quality + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { LOGW("Could not enable bilinear filtering"); } diff --git a/app/src/server.c b/app/src/server.c index d5d8b673..33f25b55 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "config.h" #include "log.h" @@ -37,17 +38,45 @@ static SDL_bool remove_server(const char *serial) { return process_check_success(process, "adb shell rm"); } -static SDL_bool enable_tunnel(const char *serial, Uint16 local_port) { +static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) { process_t process = adb_reverse(serial, SOCKET_NAME, local_port); return process_check_success(process, "adb reverse"); } -static SDL_bool disable_tunnel(const char *serial) { +static SDL_bool disable_tunnel_reverse(const char *serial) { process_t process = adb_reverse_remove(serial, SOCKET_NAME); return process_check_success(process, "adb reverse --remove"); } -static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate) { +static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) { + process_t process = adb_forward(serial, local_port, SOCKET_NAME); + return process_check_success(process, "adb forward"); +} + +static SDL_bool disable_tunnel_forward(const char *serial, Uint16 local_port) { + process_t process = adb_forward_remove(serial, local_port); + return process_check_success(process, "adb forward --remove"); +} + +static SDL_bool enable_tunnel(struct server *server) { + if (enable_tunnel_reverse(server->serial, server->local_port)) { + return SDL_TRUE; + } + + LOGW("'adb reverse' failed, fallback to 'adb forward'"); + server->tunnel_forward = SDL_TRUE; + return enable_tunnel_forward(server->serial, server->local_port); +} + +static SDL_bool disable_tunnel(struct server *server) { + if (server->tunnel_forward) { + return disable_tunnel_forward(server->serial, server->local_port); + } + return disable_tunnel_reverse(server->serial); +} + +static process_t execute_server(const char *serial, + Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) { char max_size_string[6]; char bit_rate_string[11]; sprintf(max_size_string, "%"PRIu16, max_size); @@ -60,15 +89,48 @@ static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_ "com.genymobile.scrcpy.Server", max_size_string, bit_rate_string, + tunnel_forward ? "true" : "false", }; return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } -static socket_t listen_on_port(Uint16 port) { #define IPV4_LOCALHOST 0x7F000001 + +static socket_t listen_on_port(Uint16 port) { return net_listen(IPV4_LOCALHOST, port, 1); } +static socket_t connect_and_read_byte(Uint16 port) { + socket_t socket = net_connect(IPV4_LOCALHOST, port); + if (socket == INVALID_SOCKET) { + return INVALID_SOCKET; + } + + char byte; + // the connection may succeed even if the server behind the "adb tunnel" + // is not listening, so read one byte to detect a working connection + if (net_recv_all(socket, &byte, 1) != 1) { + // the server is not listening yet behind the adb tunnel + return INVALID_SOCKET; + } + return socket; +} + +static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) { + do { + LOGD("Remaining connection attempts: %d", (int) attempts); + socket_t socket = connect_and_read_byte(port); + if (socket != INVALID_SOCKET) { + // it worked! + return socket; + } + if (attempts) { + SDL_Delay(delay); + } + } while (--attempts > 0); + return INVALID_SOCKET; +} + static void close_socket(socket_t *socket) { SDL_assert(*socket != INVALID_SOCKET); net_shutdown(*socket, SHUT_RDWR); @@ -85,63 +147,84 @@ void server_init(struct server *server) { SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate) { + server->local_port = local_port; + + if (serial) { + server->serial = SDL_strdup(serial); + } + if (!push_server(serial)) { return SDL_FALSE; } server->server_copied_to_device = SDL_TRUE; - if (!enable_tunnel(serial, local_port)) { + if (!enable_tunnel(server)) { return SDL_FALSE; } - // At the application level, the device part is "the server" because it - // serves video stream and control. However, at the network level, the - // client listens and the server connects to the client. That way, the - // client can listen before starting the server app, so there is no need to - // try to connect until the server socket is listening on the device. + // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to + // "adb forward", so the app socket is the client + if (!server->tunnel_forward) { + // At the application level, the device part is "the server" because it + // serves video stream and control. However, at the network level, the + // client listens and the server connects to the client. That way, the + // client can listen before starting the server app, so there is no need to + // try to connect until the server socket is listening on the device. - server->server_socket = listen_on_port(local_port); - if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, local_port); - disable_tunnel(serial); - return SDL_FALSE; + server->server_socket = listen_on_port(local_port); + if (server->server_socket == INVALID_SOCKET) { + LOGE("Could not listen on port %" PRIu16, local_port); + disable_tunnel(server); + return SDL_FALSE; + } } // server will connect to our server socket - server->process = execute_server(serial, max_size, bit_rate); + server->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward); if (server->process == PROCESS_NONE) { - close_socket(&server->server_socket); - disable_tunnel(serial); + if (!server->tunnel_forward) { + close_socket(&server->server_socket); + } + disable_tunnel(server); return SDL_FALSE; } - server->adb_reverse_enabled = SDL_TRUE; + server->tunnel_enabled = SDL_TRUE; return SDL_TRUE; } -socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms) { - server->device_socket = net_accept(server->server_socket); +socket_t server_connect_to(struct server *server, Uint32 timeout_ms) { + if (!server->tunnel_forward) { + server->device_socket = net_accept(server->server_socket); + } else { + Uint32 attempts = 10; + Uint32 delay = 100; // ms + server->device_socket = connect_to_server(server->local_port, attempts, delay); + } + if (server->device_socket == INVALID_SOCKET) { return INVALID_SOCKET; } - // we don't need the server socket anymore - close_socket(&server->server_socket); + if (!server->tunnel_forward) { + // we don't need the server socket anymore + close_socket(&server->server_socket); + } // the server is started, we can clean up the jar from the temporary folder - remove_server(serial); // ignore failure + remove_server(server->serial); // ignore failure server->server_copied_to_device = SDL_FALSE; // we don't need the adb tunnel anymore - disable_tunnel(serial); // ignore failure - server->adb_reverse_enabled = SDL_FALSE; + disable_tunnel(server); // ignore failure + server->tunnel_enabled = SDL_FALSE; return server->device_socket; } -void server_stop(struct server *server, const char *serial) { +void server_stop(struct server *server) { SDL_assert(server->process != PROCESS_NONE); if (!cmd_terminate(server->process)) { @@ -151,13 +234,13 @@ void server_stop(struct server *server, const char *serial) { cmd_simple_wait(server->process, NULL); // ignore exit code LOGD("Server terminated"); - if (server->adb_reverse_enabled) { + if (server->tunnel_enabled) { // ignore failure - disable_tunnel(serial); + disable_tunnel(server); } if (server->server_copied_to_device) { - remove_server(serial); // ignore failure + remove_server(server->serial); // ignore failure } } @@ -168,4 +251,5 @@ void server_destroy(struct server *server) { if (server->device_socket != INVALID_SOCKET) { close_socket(&server->device_socket); } + SDL_free((void *) server->serial); } diff --git a/app/src/server.h b/app/src/server.h index c2fe1125..fcd44b8d 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -5,18 +5,24 @@ #include "net.h" struct server { + const char *serial; process_t process; - socket_t server_socket; + socket_t server_socket; // only used if !tunnel_forward socket_t device_socket; - SDL_bool adb_reverse_enabled; + Uint16 local_port; + SDL_bool tunnel_enabled; + SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse" SDL_bool server_copied_to_device; }; #define SERVER_INITIALIZER { \ + .serial = NULL, \ .process = PROCESS_NONE, \ .server_socket = INVALID_SOCKET, \ .device_socket = INVALID_SOCKET, \ - .adb_reverse_enabled = SDL_FALSE, \ + .local_port = 0, \ + .tunnel_enabled = SDL_FALSE, \ + .tunnel_forward = SDL_FALSE, \ .server_copied_to_device = SDL_FALSE, \ } @@ -28,10 +34,10 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po Uint16 max_size, Uint32 bit_rate); // block until the communication with the server is established -socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms); +socket_t server_connect_to(struct server *server, Uint32 timeout_ms); // disconnect and kill the server process -void server_stop(struct server *server, const char *serial); +void server_stop(struct server *server); // close and release sockets void server_destroy(struct server *server); diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index b983fea9..112ea8b8 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -35,16 +35,36 @@ static void test_serialize_text_event() { unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; int size = control_event_serialize(&event, buf); - assert(size == 15); + assert(size == 16); const unsigned char expected[] = { 0x01, // CONTROL_EVENT_TYPE_KEYCODE - 0x0d, // text length + 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_long_text_event() { + struct control_event event; + event.type = CONTROL_EVENT_TYPE_TEXT; + char text[TEXT_MAX_LENGTH]; + memset(text, 'a', sizeof(text)); + event.text_event.text = text; + + unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 3 + sizeof(text)); + + unsigned char expected[3 + TEXT_MAX_LENGTH]; + expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE + expected[1] = 0x01; + expected[2] = 0x2c; // text length (16 bits) + memset(&expected[3], 'a', TEXT_MAX_LENGTH); + + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_mouse_event() { struct control_event event = { .type = CONTROL_EVENT_TYPE_MOUSE, @@ -114,6 +134,7 @@ static void test_serialize_scroll_event() { int main() { test_serialize_keycode_event(); test_serialize_text_event(); + test_serialize_long_text_event(); test_serialize_mouse_event(); test_serialize_scroll_event(); return 0; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java index 43b8f930..3c9cbda1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java @@ -11,7 +11,7 @@ public final class ControlEvent { public static final int TYPE_SCROLL = 3; public static final int TYPE_COMMAND = 4; - public static final int COMMAND_SCREEN_ON = 0; + public static final int COMMAND_BACK_OR_SCREEN_ON = 0; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index c1250896..83088b10 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -13,8 +13,8 @@ public class ControlEventReader { private static final int SCROLL_PAYLOAD_LENGTH = 16; private static final int COMMAND_PAYLOAD_LENGTH = 1; - private static final int TEXT_MAX_LENGTH = 256; - private static final int RAW_BUFFER_SIZE = 128; + public static final int TEXT_MAX_LENGTH = 300; + private static final int RAW_BUFFER_SIZE = 1024; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); @@ -94,7 +94,7 @@ public class ControlEventReader { if (buffer.remaining() < 1) { return null; } - int len = toUnsigned(buffer.get()); + int len = toUnsigned(buffer.getShort()); if (buffer.remaining() < len) { return null; } diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 203ecc9b..d5740c15 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import android.net.LocalServerSocket; import android.net.LocalSocket; import android.net.LocalSocketAddress; @@ -33,8 +34,24 @@ public final class DesktopConnection implements Closeable { return localSocket; } - public static DesktopConnection open(Device device) throws IOException { - LocalSocket socket = connect(SOCKET_NAME); + private static LocalSocket listenAndAccept(String abstractName) throws IOException { + LocalServerSocket localServerSocket = new LocalServerSocket(abstractName); + try { + return localServerSocket.accept(); + } finally { + localServerSocket.close(); + } + } + + public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { + LocalSocket socket; + if (tunnelForward) { + socket = listenAndAccept(SOCKET_NAME); + // send one byte so the client may read() to detect a connection error + socket.getOutputStream().write(0); + } else { + socket = connect(SOCKET_NAME); + } DesktopConnection connection = new DesktopConnection(socket); Size videoSize = device.getScreenInfo().getVideoSize(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index d78ea1b3..07c15e2b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -50,8 +50,8 @@ public final class Device { DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); boolean rotated = (displayInfo.getRotation() & 1) != 0; Size deviceSize = displayInfo.getSize(); - int w = deviceSize.getWidth(); - int h = deviceSize.getHeight(); + int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8 + int h = deviceSize.getHeight() & ~7; if (maxSize > 0) { if (BuildConfig.DEBUG && maxSize % 8 != 0) { throw new AssertionError("Max size must be a multiple of 8"); diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index a7733f96..547e20ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -39,10 +39,6 @@ public class EventController { coords.orientation = 0; coords.pressure = 1; coords.size = 1; - coords.toolMajor = 1; - coords.toolMinor = 1; - coords.touchMajor = 1; - coords.touchMinor = 1; } private void setPointerCoords(Point point) { @@ -167,10 +163,15 @@ public class EventController { return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER); } + private boolean pressBackOrTurnScreenOn() { + int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; + return injectKeycode(keycode); + } + private boolean executeCommand(int action) { switch (action) { - case ControlEvent.COMMAND_SCREEN_ON: - return turnScreenOn(); + case ControlEvent.COMMAND_BACK_OR_SCREEN_ON: + return pressBackOrTurnScreenOn(); default: Ln.w("Unsupported command: " + action); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 6ab2f694..332463e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; public class Options { private int maxSize; private int bitRate; + private boolean tunnelForward; public int getMaxSize() { return maxSize; @@ -19,4 +20,12 @@ public class Options { public void setBitRate(int bitRate) { this.bitRate = bitRate; } + + public boolean isTunnelForward() { + return tunnelForward; + } + + public void setTunnelForward(boolean tunnelForward) { + this.tunnelForward = tunnelForward; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 5df436d1..d7294ae0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -10,7 +10,8 @@ public final class Server { private static void scrcpy(Options options) throws IOException { final Device device = new Device(options); - try (DesktopConnection connection = DesktopConnection.open(device)) { + boolean tunnelForward = options.isTunnelForward(); + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate()); // asynchronous @@ -55,6 +56,13 @@ public final class Server { int bitRate = Integer.parseInt(args[1]); options.setBitRate(bitRate); + if (args.length < 3) { + return options; + } + // use "adb forward" instead of "adb tunnel"? (so the server must listen) + boolean tunnelForward = Boolean.parseBoolean(args[2]); + options.setTunnelForward(tunnelForward); + return options; } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java index 4d9952d0..d2b10ccb 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java @@ -3,14 +3,15 @@ package com.genymobile.scrcpy; import android.view.KeyEvent; import android.view.MotionEvent; -import org.junit.Assert; -import org.junit.Test; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Test; public class ControlEventReaderTest { @@ -43,8 +44,8 @@ public class ControlEventReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlEvent.TYPE_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeByte(text.length); - dos.write("testé".getBytes(StandardCharsets.UTF_8)); + dos.writeShort(text.length); + dos.write(text); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); @@ -54,6 +55,26 @@ public class ControlEventReaderTest { Assert.assertEquals("testé", event.getText()); } + @Test + public void testParseLongTextEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_TEXT); + byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH]; + Arrays.fill(text, (byte) 'a'); + dos.writeShort(text.length); + dos.write(text); + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType()); + Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); + } + @Test public void testParseMouseEvent() throws IOException { ControlEventReader reader = new ControlEventReader();