From 3eda38e5fc63f08eed408c39155b88e1bcefc341 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Mar 2019 18:45:29 +0100 Subject: [PATCH 01/90] Do not call codec.stop() on exception On exception, the codec is not in a state were .stop() can be called. --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 22eb6a54..71f5ab10 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -71,8 +71,9 @@ public class ScreenEncoder implements Device.RotationListener { codec.start(); try { alive = encode(codec, fd); - } finally { + // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); + } finally { destroyDisplay(display); codec.release(); surface.release(); From 668e54fd4b5ecf6fa03b60c2c59446538b99c976 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 May 2019 14:39:50 +0200 Subject: [PATCH 02/90] Upgrade gradle --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a96495fc..1b6f5aef 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' + classpath 'com.android.tools.build:gradle:3.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8ba12c0c..33997651 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 04 11:48:32 CEST 2018 +#Thu Apr 18 11:45:59 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip From 2837c6eaab51eac7bc18c5a0848754a203970f44 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 May 2019 14:40:29 +0200 Subject: [PATCH 03/90] Add method to log error without throwable Add Ln.e(message) in addition to Ln.e(message, error). --- server/src/main/java/com/genymobile/scrcpy/Ln.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 9364519e..cd466b3e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -52,7 +52,13 @@ public final class Ln { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); System.out.println("ERROR: " + message); - throwable.printStackTrace(); + if (throwable != null) { + throwable.printStackTrace(); + } } } + + public static void e(String message) { + e(message, null); + } } From c8338b2918d81d0238a7254ec2a6556211beb957 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 May 2019 14:41:38 +0200 Subject: [PATCH 04/90] Recover if expand/collapse panels is not available Some devices don't have the required method. Recover gracefully without crashing the server. Fixes . --- .../scrcpy/wrappers/StatusBarManager.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 8f79f51f..e8723ff5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -2,38 +2,50 @@ package com.genymobile.scrcpy.wrappers; import android.os.IInterface; +import com.genymobile.scrcpy.Ln; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class StatusBarManager { private final IInterface manager; - private final Method expandNotificationsPanelMethod; - private final Method collapsePanelsMethod; + private Method expandNotificationsPanelMethod; + private Method collapsePanelsMethod; public StatusBarManager(IInterface manager) { this.manager = manager; - try { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); - collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); - } } public void expandNotificationsPanel() { + if (expandNotificationsPanelMethod == null) { + try { + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + } catch (NoSuchMethodException e) { + Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); + return; + } + } try { expandNotificationsPanelMethod.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e); } } public void collapsePanels() { + if (collapsePanelsMethod == null) { + try { + collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); + } catch (NoSuchMethodException e) { + Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); + return; + } + } try { collapsePanelsMethod.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e); } } } From 1630f923ef9847142839907a9c2f3c3bde3a3369 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 19 May 2019 09:00:15 +0800 Subject: [PATCH 05/90] Return success count in injectText It will insert as many text as possible now. Fix #509, tested on Windows 10 and Arch Linux. Signed-off-by: Yu-Chen Lin --- .../main/java/com/genymobile/scrcpy/EventController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 341869fa..87aee61b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -104,13 +104,15 @@ public class EventController { return true; } - private boolean injectText(String text) { + private int injectText(String text) { + int successCount = 0; for (char c : text.toCharArray()) { if (!injectChar(c)) { - return false; + continue; } + successCount++; } - return true; + return successCount; } private boolean injectMouse(int action, int buttons, Position position) { From 3068457b901964cacd57405decd5852a1d1ce8fe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 May 2019 08:33:41 +0200 Subject: [PATCH 06/90] Log characters failed to be injected Some characters may not be injected (e.g. '\r`). Log them instead of ignoring them silently. --- server/src/main/java/com/genymobile/scrcpy/EventController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 87aee61b..9885e04b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -108,6 +108,7 @@ public class EventController { int successCount = 0; for (char c : text.toCharArray()) { if (!injectChar(c)) { + Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); continue; } successCount++; From 7ed976967f9bb14670b0030db2c28e4b36bd3f95 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 00:22:05 +0200 Subject: [PATCH 07/90] Fix checkstyle warning Checkstyle wants a specific order of imports. --- .../java/com/genymobile/scrcpy/wrappers/StatusBarManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index e8723ff5..74003b64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.wrappers; -import android.os.IInterface; - import com.genymobile.scrcpy.Ln; +import android.os.IInterface; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; From 3bc1c51b91dbad1e67bb93371599049217508890 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 00:10:45 +0200 Subject: [PATCH 08/90] Always use SDL_malloc() and SDL_free() To avoid mixing SDL_malloc()/SDL_strdup() with free(), or malloc() with SDL_free(), always use the SDL version. --- app/src/command.c | 8 ++++---- app/src/str_util.c | 6 ++++-- app/src/stream.c | 4 ++-- app/src/sys/win/command.c | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/command.c b/app/src/command.c index 2c6d45aa..717455dd 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -147,7 +147,7 @@ adb_push(const char *serial, const char *local, const char *remote) { } remote = strquote(remote); if (!remote) { - free((void *) local); + SDL_free((void *) local); return PROCESS_NONE; } #endif @@ -156,8 +156,8 @@ adb_push(const char *serial, const char *local, const char *remote) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - free((void *) remote); - free((void *) local); + SDL_free((void *) remote); + SDL_free((void *) local); #endif return proc; @@ -178,7 +178,7 @@ adb_install(const char *serial, const char *local) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - free((void *) local); + SDL_free((void *) local); #endif return proc; diff --git a/app/src/str_util.c b/app/src/str_util.c index 3509331a..d9ae6948 100644 --- a/app/src/str_util.c +++ b/app/src/str_util.c @@ -8,6 +8,8 @@ # include #endif +#include + size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; @@ -45,7 +47,7 @@ truncated: char * strquote(const char *src) { size_t len = strlen(src); - char *quoted = malloc(len + 3); + char *quoted = SDL_malloc(len + 3); if (!quoted) { return NULL; } @@ -65,7 +67,7 @@ utf8_to_wide_char(const char *utf8) { return NULL; } - wchar_t *wide = malloc(len * sizeof(wchar_t)); + wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); if (!wide) { return NULL; } diff --git a/app/src/stream.c b/app/src/stream.c index 7ed95ee8..0e751eb3 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -24,7 +24,7 @@ static struct frame_meta * frame_meta_new(uint64_t pts) { - struct frame_meta *meta = malloc(sizeof(*meta)); + struct frame_meta *meta = SDL_malloc(sizeof(*meta)); if (!meta) { return meta; } @@ -35,7 +35,7 @@ frame_meta_new(uint64_t pts) { static void frame_meta_delete(struct frame_meta *frame_meta) { - free(frame_meta); + SDL_free(frame_meta); } static bool diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 1cd7274f..8434dc98 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -44,7 +44,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { #endif if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi)) { - free(wide); + SDL_free(wide); *handle = NULL; if (GetLastError() == ERROR_FILE_NOT_FOUND) { return PROCESS_ERROR_MISSING_BINARY; @@ -52,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { return PROCESS_ERROR_GENERIC; } - free(wide); + SDL_free(wide); *handle = pi.hProcess; return PROCESS_SUCCESS; } From 08f506b24f8c1a5fe3a2c12ab42710633150b4ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 08:19:49 +0200 Subject: [PATCH 09/90] Replace SDL_bool by bool in tests Commit dfed1b250e5aada1c0b4c75fbaf54aebc19a92d5 replaced SDL types by standard types in sources, but tests were not updated. --- app/tests/test_control_event_queue.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/tests/test_control_event_queue.c b/app/tests/test_control_event_queue.c index a27181b6..432f6cec 100644 --- a/app/tests/test_control_event_queue.c +++ b/app/tests/test_control_event_queue.c @@ -5,21 +5,21 @@ static void test_control_event_queue_empty(void) { struct control_event_queue queue; - SDL_bool init_ok = control_event_queue_init(&queue); + bool init_ok = control_event_queue_init(&queue); assert(init_ok); assert(control_event_queue_is_empty(&queue)); struct control_event dummy_event; - SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event); + bool push_ok = control_event_queue_push(&queue, &dummy_event); assert(push_ok); assert(!control_event_queue_is_empty(&queue)); - SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); + bool take_ok = control_event_queue_take(&queue, &dummy_event); assert(take_ok); assert(control_event_queue_is_empty(&queue)); - SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event); + bool take_empty_ok = control_event_queue_take(&queue, &dummy_event); assert(!take_empty_ok); // the queue is empty control_event_queue_destroy(&queue); @@ -27,7 +27,7 @@ static void test_control_event_queue_empty(void) { static void test_control_event_queue_full(void) { struct control_event_queue queue; - SDL_bool init_ok = control_event_queue_init(&queue); + bool init_ok = control_event_queue_init(&queue); assert(init_ok); assert(!control_event_queue_is_full(&queue)); @@ -36,7 +36,7 @@ static void test_control_event_queue_full(void) { // fill the queue while (control_event_queue_push(&queue, &dummy_event)); - SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); + bool take_ok = control_event_queue_take(&queue, &dummy_event); assert(take_ok); assert(!control_event_queue_is_full(&queue)); @@ -45,7 +45,7 @@ static void test_control_event_queue_full(void) { static void test_control_event_queue_push_take(void) { struct control_event_queue queue; - SDL_bool init_ok = control_event_queue_init(&queue); + bool init_ok = control_event_queue_init(&queue); assert(init_ok); struct control_event event = { @@ -57,7 +57,7 @@ static void test_control_event_queue_push_take(void) { }, }; - SDL_bool push1_ok = control_event_queue_push(&queue, &event); + bool push1_ok = control_event_queue_push(&queue, &event); assert(push1_ok); event = (struct control_event) { @@ -67,11 +67,11 @@ static void test_control_event_queue_push_take(void) { }, }; - SDL_bool push2_ok = control_event_queue_push(&queue, &event); + bool push2_ok = control_event_queue_push(&queue, &event); assert(push2_ok); // overwrite event - SDL_bool take1_ok = control_event_queue_take(&queue, &event); + bool take1_ok = control_event_queue_take(&queue, &event); assert(take1_ok); assert(event.type == CONTROL_EVENT_TYPE_KEYCODE); assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN); @@ -79,7 +79,7 @@ static void test_control_event_queue_push_take(void) { assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON)); // overwrite event - SDL_bool take2_ok = control_event_queue_take(&queue, &event); + bool take2_ok = control_event_queue_take(&queue, &event); assert(take2_ok); assert(event.type == CONTROL_EVENT_TYPE_TEXT); assert(!strcmp(event.text_event.text, "abc")); From 0fbab42f8cf9d392b769605226588fc21c6f2a09 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 08:20:05 +0200 Subject: [PATCH 10/90] Format meson.build for readability --- app/meson.build | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/meson.build b/app/meson.build index 5942fd08..d22cb63f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -143,18 +143,34 @@ else link_args = [] endif -executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: c_args, link_args: link_args) +executable('scrcpy', src, + dependencies: dependencies, + include_directories: src_dir, + install: true, + c_args: c_args, + link_args: link_args) ### TESTS tests = [ - ['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/control_event.c']], - ['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/control_event.c']], - ['test_strutil', ['tests/test_strutil.c', 'src/str_util.c']], + ['test_control_event_queue', [ + 'tests/test_control_event_queue.c', + 'src/control_event.c' + ]], + ['test_control_event_serialize', [ + 'tests/test_control_event_serialize.c', + 'src/control_event.c' + ]], + ['test_strutil', [ + 'tests/test_strutil.c', + 'src/str_util.c' + ]], ] foreach t : tests - exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies) + exe = executable(t[0], t[1], + include_directories: src_dir, + dependencies: dependencies) test(t[0], exe) endforeach From d2504f974c05a7f15efda631decd1e6b31a0981e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 May 2019 13:37:27 +0200 Subject: [PATCH 11/90] Fix indentation Previous refactorings broke indentation. --- app/src/scrcpy.c | 3 +-- app/src/server.h | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b777b770..4a0c045c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -421,8 +421,7 @@ finally_destroy_server: wait_show_touches(proc_show_touches); } LOGI("Disable show_touches"); - proc_show_touches = set_show_touches_enabled(options->serial, - false); + proc_show_touches = set_show_touches_enabled(options->serial, false); wait_show_touches(proc_show_touches); } diff --git a/app/src/server.h b/app/src/server.h index 0f25d48f..c72dbdd6 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -18,12 +18,12 @@ struct server { bool send_frame_meta; // request frame PTS to be able to record properly }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .server_socket = INVALID_SOCKET, \ - .device_socket = INVALID_SOCKET, \ - .local_port = 0, \ +#define SERVER_INITIALIZER { \ + .serial = NULL, \ + .process = PROCESS_NONE, \ + .server_socket = INVALID_SOCKET, \ + .device_socket = INVALID_SOCKET, \ + .local_port = 0, \ .tunnel_enabled = false, \ .tunnel_forward = false, \ .send_frame_meta = false, \ @@ -36,8 +36,8 @@ server_init(struct server *server); // push, enable tunnel et start the server bool server_start(struct server *server, const char *serial, - uint16_t local_port, uint16_t max_size, uint32_t bit_rate, - const char *crop, bool send_frame_meta); + uint16_t local_port, uint16_t max_size, uint32_t bit_rate, + const char *crop, bool send_frame_meta); // block until the communication with the server is established socket_t From befe455e44620bd5a0f05c3e0040691fffd77fd2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 08:24:32 +0200 Subject: [PATCH 12/90] Remove unused includes The struct control_event does not use mutexes, and net.h does not need SDL_platform.h. --- app/src/control_event.c | 1 - app/src/control_event.h | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/control_event.c b/app/src/control_event.c index 40de6efc..bdd044a0 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -3,7 +3,6 @@ #include #include "buffer_util.h" -#include "lock_util.h" #include "log.h" static void diff --git a/app/src/control_event.h b/app/src/control_event.h index 2a33244b..a7bde313 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -3,7 +3,6 @@ #include #include -#include #include "android/input.h" #include "android/keycodes.h" From 999c9646895fea64ece612be86aed162472b5d3b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 08:27:10 +0200 Subject: [PATCH 13/90] Make macro expansion-safe Use parentheses to avoid unexpected results. For example, make: 2 * SERIALIZED_EVENT_MAX_SIZE expand to: 2 * (3 + TEXT_MAX_LENGTH) instead of: 2 * 3 + TEXT_MAX_LENGTH --- app/src/control_event.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/control_event.h b/app/src/control_event.h index a7bde313..ac698065 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -10,7 +10,7 @@ #define CONTROL_EVENT_QUEUE_SIZE 64 #define TEXT_MAX_LENGTH 300 -#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH +#define SERIALIZED_EVENT_MAX_SIZE (3 + TEXT_MAX_LENGTH) enum control_event_type { CONTROL_EVENT_TYPE_KEYCODE, From b08dada6c1f0d93392886b0e874e0e486e5fb447 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 08:35:28 +0200 Subject: [PATCH 14/90] Prefix control event constants by namespace This will avoid conflicts with future device events. --- app/src/control_event.c | 4 ++-- app/src/control_event.h | 6 +++--- app/src/controller.c | 2 +- app/tests/test_control_event_serialize.c | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/control_event.c b/app/src/control_event.c index bdd044a0..e836f506 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -25,9 +25,9 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { case CONTROL_EVENT_TYPE_TEXT: { // write length (2 bytes) + string (non nul-terminated) size_t len = strlen(event->text_event.text); - if (len > TEXT_MAX_LENGTH) { + if (len > CONTROL_EVENT_TEXT_MAX_LENGTH) { // injecting a text takes time, so limit the text length - len = TEXT_MAX_LENGTH; + len = CONTROL_EVENT_TEXT_MAX_LENGTH; } buffer_write16be(&buf[1], (uint16_t) len); memcpy(&buf[3], event->text_event.text, len); diff --git a/app/src/control_event.h b/app/src/control_event.h index ac698065..3fc1a54f 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -9,8 +9,8 @@ #include "common.h" #define CONTROL_EVENT_QUEUE_SIZE 64 -#define TEXT_MAX_LENGTH 300 -#define SERIALIZED_EVENT_MAX_SIZE (3 + TEXT_MAX_LENGTH) +#define CONTROL_EVENT_TEXT_MAX_LENGTH 300 +#define CONTROL_EVENT_SERIALIZED_MAX_SIZE (3 + CONTROL_EVENT_TEXT_MAX_LENGTH) enum control_event_type { CONTROL_EVENT_TYPE_KEYCODE, @@ -59,7 +59,7 @@ struct control_event_queue { int tail; }; -// buf size must be at least SERIALIZED_EVENT_MAX_SIZE +// buf size must be at least CONTROL_EVENT_SERIALIZED_MAX_SIZE int control_event_serialize(const struct control_event *event, unsigned char *buf); diff --git a/app/src/controller.c b/app/src/controller.c index 8b95e88c..8a342503 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -51,7 +51,7 @@ controller_push_event(struct controller *controller, static bool process_event(struct controller *controller, const struct control_event *event) { - unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE]; + unsigned char serialized_event[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; int length = control_event_serialize(event, serialized_event); if (!length) { return false; diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index 90b75fff..9fae75dc 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -13,7 +13,7 @@ static void test_serialize_keycode_event(void) { }, }; - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; int size = control_event_serialize(&event, buf); assert(size == 10); @@ -34,7 +34,7 @@ static void test_serialize_text_event(void) { }, }; - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; int size = control_event_serialize(&event, buf); assert(size == 16); @@ -49,20 +49,20 @@ static void test_serialize_text_event(void) { static void test_serialize_long_text_event(void) { struct control_event event; event.type = CONTROL_EVENT_TYPE_TEXT; - char text[TEXT_MAX_LENGTH + 1]; + char text[CONTROL_EVENT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', sizeof(text)); - text[TEXT_MAX_LENGTH] = '\0'; + text[CONTROL_EVENT_TEXT_MAX_LENGTH] = '\0'; event.text_event.text = text; - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; int size = control_event_serialize(&event, buf); - assert(size == 3 + TEXT_MAX_LENGTH); + assert(size == 3 + CONTROL_EVENT_TEXT_MAX_LENGTH); - unsigned char expected[3 + TEXT_MAX_LENGTH]; + unsigned char expected[3 + CONTROL_EVENT_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); + memset(&expected[3], 'a', CONTROL_EVENT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } @@ -86,7 +86,7 @@ static void test_serialize_mouse_event(void) { }, }; - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; int size = control_event_serialize(&event, buf); assert(size == 18); @@ -119,7 +119,7 @@ static void test_serialize_scroll_event(void) { }, }; - unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; int size = control_event_serialize(&event, buf); assert(size == 21); From e1afd9f8b019607fcb7f28f42682959697456f68 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 08:38:32 +0200 Subject: [PATCH 15/90] Fix event ownership comment --- app/src/control_event.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/control_event.h b/app/src/control_event.h index 3fc1a54f..70d8d494 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -75,7 +75,7 @@ control_event_queue_is_empty(const struct control_event_queue *queue); bool control_event_queue_is_full(const struct control_event_queue *queue); -// event is copied, the queue does not use the event after the function returns +// the event is "moved": the queue takes ownership of its fields bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event); From 507b0bcccf708ef0a4b732d9165e882df4330bd9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 21:33:34 +0200 Subject: [PATCH 16/90] Fix memory leak on error The variable condition was not destroyed on strdup() failure. --- app/src/file_handler.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index c72b598d..79763c6e 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -100,6 +100,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial) { file_handler->serial = SDL_strdup(serial); if (!file_handler->serial) { LOGW("Cannot strdup serial"); + SDL_DestroyCond(file_handler->event_cond); SDL_DestroyMutex(file_handler->mutex); return false; } From bf5e54b2e9c55186d0cf34098506a270a3f416a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 17:26:21 +0200 Subject: [PATCH 17/90] Make control_event_serialize() return size_t control_event_serialize() returns the number of bytes written, so the type should be size_t. --- app/src/control_event.c | 2 +- app/src/control_event.h | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/control_event.c b/app/src/control_event.c index e836f506..ea6585a4 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -13,7 +13,7 @@ write_position(uint8_t *buf, const struct position *position) { buffer_write16be(&buf[10], position->screen_size.height); } -int +size_t control_event_serialize(const struct control_event *event, unsigned char *buf) { buf[0] = event->type; switch (event->type) { diff --git a/app/src/control_event.h b/app/src/control_event.h index 70d8d494..49ec7020 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -2,6 +2,7 @@ #define CONTROLEVENT_H #include +#include #include #include "android/input.h" @@ -60,7 +61,8 @@ struct control_event_queue { }; // buf size must be at least CONTROL_EVENT_SERIALIZED_MAX_SIZE -int +// return the number of bytes written +size_t control_event_serialize(const struct control_event *event, unsigned char *buf); bool From 7fc8793d5b98a8a7c2cf208227c5cb966513171d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 17:45:46 +0200 Subject: [PATCH 18/90] Make buffer util functions accept const buffers So that they can be used both on const and non-const input buffers. --- app/src/buffer_util.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 5d94deef..4bfd08d3 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -19,12 +19,12 @@ buffer_write32be(uint8_t *buf, uint32_t value) { } static inline uint32_t -buffer_read32be(uint8_t *buf) { +buffer_read32be(const uint8_t *buf) { return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } static inline -uint64_t buffer_read64be(uint8_t *buf) { +uint64_t buffer_read64be(const uint8_t *buf) { uint32_t msb = buffer_read32be(buf); uint32_t lsb = buffer_read32be(&buf[4]); return ((uint64_t) msb << 32) | lsb; From 7475550ae8e4f128ac829a0d178298c72f840ce3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 17:27:41 +0200 Subject: [PATCH 19/90] Add buffer_read16be() Add a function to read 16 bits in big-endian to a uint16_t. --- app/src/buffer_util.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 4bfd08d3..a79014b1 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -18,6 +18,11 @@ buffer_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline uint16_t +buffer_read16be(const uint8_t *buf) { + return (buf[0] << 8) | buf[1]; +} + static inline uint32_t buffer_read32be(const uint8_t *buf) { return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; From b38292cd695ebc51a6925f21afeb89eeff1b0208 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 20:53:21 +0200 Subject: [PATCH 20/90] Add generic circular buffer Add a circular buffer implementation, to factorize multiple specific queues implementation. --- app/meson.build | 3 ++ app/src/cbuf.h | 50 +++++++++++++++++++++++++++++ app/tests/test_cbuf.c | 73 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 app/src/cbuf.h create mode 100644 app/tests/test_cbuf.c diff --git a/app/meson.build b/app/meson.build index d22cb63f..73cf55ca 100644 --- a/app/meson.build +++ b/app/meson.build @@ -154,6 +154,9 @@ executable('scrcpy', src, ### TESTS tests = [ + ['test_cbuf', [ + 'tests/test_cbuf.c', + ]], ['test_control_event_queue', [ 'tests/test_control_event_queue.c', 'src/control_event.c' diff --git a/app/src/cbuf.h b/app/src/cbuf.h new file mode 100644 index 00000000..5d9fe4ae --- /dev/null +++ b/app/src/cbuf.h @@ -0,0 +1,50 @@ +// generic circular buffer (bounded queue) implementation +#ifndef CBUF_H +#define CBUF_H + +#include +#include + +// To define a circular buffer type of 20 ints: +// typedef CBUF(int, 20) my_cbuf_t; +// +// data has length CAP + 1 to distinguish empty vs full. +#define CBUF(TYPE, CAP) { \ + TYPE data[(CAP) + 1]; \ + size_t head; \ + size_t tail; \ +} + +#define cbuf_size_(PCBUF) \ + (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) + +#define cbuf_is_empty(PCBUF) \ + ((PCBUF)->head == (PCBUF)->tail) + +#define cbuf_is_full(PCBUF) \ + (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) + +#define cbuf_init(PCBUF) \ + (void) ((PCBUF)->head = (PCBUF)->tail = 0) + +#define cbuf_push(PCBUF, ITEM) \ + ({ \ + bool ok = !cbuf_is_full(PCBUF); \ + if (ok) { \ + (PCBUF)->data[(PCBUF)->head] = (ITEM); \ + (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ + } \ + ok; \ + }) \ + +#define cbuf_take(PCBUF, PITEM) \ + ({ \ + bool ok = !cbuf_is_empty(PCBUF); \ + if (ok) { \ + *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ + (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ + } \ + ok; \ + }) + +#endif diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c new file mode 100644 index 00000000..9d5fdc27 --- /dev/null +++ b/app/tests/test_cbuf.c @@ -0,0 +1,73 @@ +#include +#include + +#include "cbuf.h" + +struct int_queue CBUF(int, 32); + +static void test_cbuf_empty(void) { + struct int_queue queue; + cbuf_init(&queue); + + assert(cbuf_is_empty(&queue)); + + bool push_ok = cbuf_push(&queue, 42); + assert(push_ok); + assert(!cbuf_is_empty(&queue)); + + int item; + bool take_ok = cbuf_take(&queue, &item); + assert(take_ok); + assert(cbuf_is_empty(&queue)); + + bool take_empty_ok = cbuf_take(&queue, &item); + assert(!take_empty_ok); // the queue is empty +} + +static void test_cbuf_full(void) { + struct int_queue queue; + cbuf_init(&queue); + + assert(!cbuf_is_full(&queue)); + + // fill the queue + for (int i = 0; i < 32; ++i) { + bool ok = cbuf_push(&queue, i); + assert(ok); + } + bool ok = cbuf_push(&queue, 42); + assert(!ok); // the queue if full + + int item; + bool take_ok = cbuf_take(&queue, &item); + assert(take_ok); + assert(!cbuf_is_full(&queue)); +} + +static void test_cbuf_push_take(void) { + struct int_queue queue; + cbuf_init(&queue); + + bool push1_ok = cbuf_push(&queue, 42); + assert(push1_ok); + + bool push2_ok = cbuf_push(&queue, 35); + assert(push2_ok); + + int item; + + bool take1_ok = cbuf_take(&queue, &item); + assert(take1_ok); + assert(item == 42); + + bool take2_ok = cbuf_take(&queue, &item); + assert(take2_ok); + assert(item == 35); +} + +int main(void) { + test_cbuf_empty(); + test_cbuf_full(); + test_cbuf_push_take(); + return 0; +} From 241a3dcba524337b2b59c1931ed3403c00a6d05e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 21:24:30 +0200 Subject: [PATCH 21/90] Use cbuf for control event queue Replace the control_event_queue implementation by cbuf. --- app/meson.build | 4 -- app/src/control_event.c | 49 -------------- app/src/control_event.h | 28 -------- app/src/controller.c | 21 +++--- app/src/controller.h | 7 +- app/tests/test_control_event_queue.c | 95 ---------------------------- 6 files changed, 14 insertions(+), 190 deletions(-) delete mode 100644 app/tests/test_control_event_queue.c diff --git a/app/meson.build b/app/meson.build index 73cf55ca..9bcaa9ae 100644 --- a/app/meson.build +++ b/app/meson.build @@ -157,10 +157,6 @@ tests = [ ['test_cbuf', [ 'tests/test_cbuf.c', ]], - ['test_control_event_queue', [ - 'tests/test_control_event_queue.c', - 'src/control_event.c' - ]], ['test_control_event_serialize', [ 'tests/test_control_event_serialize.c', 'src/control_event.c' diff --git a/app/src/control_event.c b/app/src/control_event.c index ea6585a4..e22337fc 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -58,52 +58,3 @@ control_event_destroy(struct control_event *event) { SDL_free(event->text_event.text); } } - -bool -control_event_queue_is_empty(const struct control_event_queue *queue) { - return queue->head == queue->tail; -} - -bool -control_event_queue_is_full(const struct control_event_queue *queue) { - return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail; -} - -bool -control_event_queue_init(struct control_event_queue *queue) { - queue->head = 0; - queue->tail = 0; - // the current implementation may not fail - return true; -} - -void -control_event_queue_destroy(struct control_event_queue *queue) { - int i = queue->tail; - while (i != queue->head) { - control_event_destroy(&queue->data[i]); - i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE; - } -} - -bool -control_event_queue_push(struct control_event_queue *queue, - const struct control_event *event) { - if (control_event_queue_is_full(queue)) { - return false; - } - queue->data[queue->head] = *event; - queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE; - return true; -} - -bool -control_event_queue_take(struct control_event_queue *queue, - struct control_event *event) { - if (control_event_queue_is_empty(queue)) { - return false; - } - *event = queue->data[queue->tail]; - queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE; - return true; -} diff --git a/app/src/control_event.h b/app/src/control_event.h index 49ec7020..13533316 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -9,7 +9,6 @@ #include "android/keycodes.h" #include "common.h" -#define CONTROL_EVENT_QUEUE_SIZE 64 #define CONTROL_EVENT_TEXT_MAX_LENGTH 300 #define CONTROL_EVENT_SERIALIZED_MAX_SIZE (3 + CONTROL_EVENT_TEXT_MAX_LENGTH) @@ -54,38 +53,11 @@ struct control_event { }; }; -struct control_event_queue { - struct control_event data[CONTROL_EVENT_QUEUE_SIZE]; - int head; - int tail; -}; - // buf size must be at least CONTROL_EVENT_SERIALIZED_MAX_SIZE // return the number of bytes written size_t control_event_serialize(const struct control_event *event, unsigned char *buf); -bool -control_event_queue_init(struct control_event_queue *queue); - -void -control_event_queue_destroy(struct control_event_queue *queue); - -bool -control_event_queue_is_empty(const struct control_event_queue *queue); - -bool -control_event_queue_is_full(const struct control_event_queue *queue); - -// the event is "moved": the queue takes ownership of its fields -bool -control_event_queue_push(struct control_event_queue *queue, - const struct control_event *event); - -bool -control_event_queue_take(struct control_event_queue *queue, - struct control_event *event); - void control_event_destroy(struct control_event *event); diff --git a/app/src/controller.c b/app/src/controller.c index 8a342503..30118218 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -8,9 +8,7 @@ bool controller_init(struct controller *controller, socket_t video_socket) { - if (!control_event_queue_init(&controller->queue)) { - return false; - } + cbuf_init(&controller->queue); if (!(controller->mutex = SDL_CreateMutex())) { return false; @@ -31,16 +29,19 @@ void controller_destroy(struct controller *controller) { SDL_DestroyCond(controller->event_cond); SDL_DestroyMutex(controller->mutex); - control_event_queue_destroy(&controller->queue); + + struct control_event event; + while (cbuf_take(&controller->queue, &event)) { + control_event_destroy(&event); + } } bool controller_push_event(struct controller *controller, const struct control_event *event) { - bool res; mutex_lock(controller->mutex); - bool was_empty = control_event_queue_is_empty(&controller->queue); - res = control_event_queue_push(&controller->queue, event); + bool was_empty = cbuf_is_empty(&controller->queue); + bool res = cbuf_push(&controller->queue, *event); if (was_empty) { cond_signal(controller->event_cond); } @@ -66,8 +67,7 @@ run_controller(void *data) { for (;;) { mutex_lock(controller->mutex); - while (!controller->stopped - && control_event_queue_is_empty(&controller->queue)) { + while (!controller->stopped && cbuf_is_empty(&controller->queue)) { cond_wait(controller->event_cond, controller->mutex); } if (controller->stopped) { @@ -76,8 +76,7 @@ run_controller(void *data) { break; } struct control_event event; - bool non_empty = control_event_queue_take(&controller->queue, - &event); + bool non_empty = cbuf_take(&controller->queue, &event); SDL_assert(non_empty); mutex_unlock(controller->mutex); diff --git a/app/src/controller.h b/app/src/controller.h index 2f7696e3..7930bf8a 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,14 +1,16 @@ #ifndef CONTROL_H #define CONTROL_H -#include "control_event.h" - #include #include #include +#include "cbuf.h" +#include "control_event.h" #include "net.h" +struct control_event_queue CBUF(struct control_event, 64); + struct controller { socket_t video_socket; SDL_Thread *thread; @@ -33,7 +35,6 @@ controller_stop(struct controller *controller); void controller_join(struct controller *controller); -// expose simple API to hide control_event_queue bool controller_push_event(struct controller *controller, const struct control_event *event); diff --git a/app/tests/test_control_event_queue.c b/app/tests/test_control_event_queue.c deleted file mode 100644 index 432f6cec..00000000 --- a/app/tests/test_control_event_queue.c +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include - -#include "control_event.h" - -static void test_control_event_queue_empty(void) { - struct control_event_queue queue; - bool init_ok = control_event_queue_init(&queue); - assert(init_ok); - - assert(control_event_queue_is_empty(&queue)); - - struct control_event dummy_event; - bool push_ok = control_event_queue_push(&queue, &dummy_event); - assert(push_ok); - assert(!control_event_queue_is_empty(&queue)); - - bool take_ok = control_event_queue_take(&queue, &dummy_event); - assert(take_ok); - assert(control_event_queue_is_empty(&queue)); - - bool take_empty_ok = control_event_queue_take(&queue, &dummy_event); - assert(!take_empty_ok); // the queue is empty - - control_event_queue_destroy(&queue); -} - -static void test_control_event_queue_full(void) { - struct control_event_queue queue; - bool init_ok = control_event_queue_init(&queue); - assert(init_ok); - - assert(!control_event_queue_is_full(&queue)); - - struct control_event dummy_event; - // fill the queue - while (control_event_queue_push(&queue, &dummy_event)); - - bool take_ok = control_event_queue_take(&queue, &dummy_event); - assert(take_ok); - assert(!control_event_queue_is_full(&queue)); - - control_event_queue_destroy(&queue); -} - -static void test_control_event_queue_push_take(void) { - struct control_event_queue queue; - bool init_ok = control_event_queue_init(&queue); - assert(init_ok); - - struct control_event event = { - .type = CONTROL_EVENT_TYPE_KEYCODE, - .keycode_event = { - .action = AKEY_EVENT_ACTION_DOWN, - .keycode = AKEYCODE_ENTER, - .metastate = AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON, - }, - }; - - bool push1_ok = control_event_queue_push(&queue, &event); - assert(push1_ok); - - event = (struct control_event) { - .type = CONTROL_EVENT_TYPE_TEXT, - .text_event = { - .text = "abc", - }, - }; - - bool push2_ok = control_event_queue_push(&queue, &event); - assert(push2_ok); - - // overwrite event - bool take1_ok = control_event_queue_take(&queue, &event); - assert(take1_ok); - assert(event.type == CONTROL_EVENT_TYPE_KEYCODE); - assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN); - assert(event.keycode_event.keycode == AKEYCODE_ENTER); - assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON)); - - // overwrite event - bool take2_ok = control_event_queue_take(&queue, &event); - assert(take2_ok); - assert(event.type == CONTROL_EVENT_TYPE_TEXT); - assert(!strcmp(event.text_event.text, "abc")); - - control_event_queue_destroy(&queue); -} - -int main(void) { - test_control_event_queue_empty(); - test_control_event_queue_full(); - test_control_event_queue_push_take(); - return 0; -} From 073181b294c27e2abd7581a9dcef0de9b1762156 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 21:46:16 +0200 Subject: [PATCH 22/90] Use cbuf for file handler request queue Replace the file_handler_request_queue implementation by cbuf. --- app/src/file_handler.c | 127 ++++++++++------------------------------- app/src/file_handler.h | 14 ++--- 2 files changed, 36 insertions(+), 105 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 79763c6e..051db897 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -2,90 +2,22 @@ #include #include + #include "config.h" #include "command.h" #include "device.h" #include "lock_util.h" #include "log.h" -struct request { - file_handler_action_t action; - char *file; -}; - -static struct request * -request_new(file_handler_action_t action, char *file) { - struct request *req = SDL_malloc(sizeof(*req)); - if (!req) { - return NULL; - } - req->action = action; - req->file = file; - return req; -} - static void -request_free(struct request *req) { - if (!req) { - return; - } +file_handler_request_destroy(struct file_handler_request *req) { SDL_free(req->file); - SDL_free(req); -} - -static bool -request_queue_is_empty(const struct request_queue *queue) { - return queue->head == queue->tail; -} - -static bool -request_queue_is_full(const struct request_queue *queue) { - return (queue->head + 1) % REQUEST_QUEUE_SIZE == queue->tail; -} - -static bool -request_queue_init(struct request_queue *queue) { - queue->head = 0; - queue->tail = 0; - return true; -} - -static void -request_queue_destroy(struct request_queue *queue) { - int i = queue->tail; - while (i != queue->head) { - request_free(queue->reqs[i]); - i = (i + 1) % REQUEST_QUEUE_SIZE; - } -} - -static bool -request_queue_push(struct request_queue *queue, struct request *req) { - if (request_queue_is_full(queue)) { - return false; - } - queue->reqs[queue->head] = req; - queue->head = (queue->head + 1) % REQUEST_QUEUE_SIZE; - return true; -} - -static bool -request_queue_take(struct request_queue *queue, struct request **req) { - if (request_queue_is_empty(queue)) { - return false; - } - // transfer ownership - *req = queue->reqs[queue->tail]; - queue->tail = (queue->tail + 1) % REQUEST_QUEUE_SIZE; - return true; } bool file_handler_init(struct file_handler *file_handler, const char *serial) { - if (!request_queue_init(&file_handler->queue)) { - return false; - } + cbuf_init(&file_handler->queue); if (!(file_handler->mutex = SDL_CreateMutex())) { return false; @@ -121,8 +53,12 @@ void file_handler_destroy(struct file_handler *file_handler) { SDL_DestroyCond(file_handler->event_cond); SDL_DestroyMutex(file_handler->mutex); - request_queue_destroy(&file_handler->queue); SDL_free(file_handler->serial); + + struct file_handler_request req; + while (cbuf_take(&file_handler->queue, &req)) { + file_handler_request_destroy(&req); + } } static process_t @@ -137,10 +73,7 @@ push_file(const char *serial, const char *file) { bool file_handler_request(struct file_handler *file_handler, - file_handler_action_t action, - char *file) { - bool res; - + file_handler_action_t action, char *file) { // start file_handler if it's used for the first time if (!file_handler->initialized) { if (!file_handler_start(file_handler)) { @@ -151,15 +84,14 @@ file_handler_request(struct file_handler *file_handler, LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", file); - struct request *req = request_new(action, file); - if (!req) { - LOGE("Could not create request"); - return false; - } + struct file_handler_request req = { + .action = action, + .file = file, + }; mutex_lock(file_handler->mutex); - bool was_empty = request_queue_is_empty(&file_handler->queue); - res = request_queue_push(&file_handler->queue, req); + bool was_empty = cbuf_is_empty(&file_handler->queue); + bool res = cbuf_push(&file_handler->queue, req); if (was_empty) { cond_signal(file_handler->event_cond); } @@ -174,8 +106,7 @@ run_file_handler(void *data) { for (;;) { mutex_lock(file_handler->mutex); file_handler->current_process = PROCESS_NONE; - while (!file_handler->stopped - && request_queue_is_empty(&file_handler->queue)) { + while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { cond_wait(file_handler->event_cond, file_handler->mutex); } if (file_handler->stopped) { @@ -183,36 +114,36 @@ run_file_handler(void *data) { mutex_unlock(file_handler->mutex); break; } - struct request *req; - bool non_empty = request_queue_take(&file_handler->queue, &req); + struct file_handler_request req; + bool non_empty = cbuf_take(&file_handler->queue, &req); SDL_assert(non_empty); process_t process; - if (req->action == ACTION_INSTALL_APK) { - LOGI("Installing %s...", req->file); - process = install_apk(file_handler->serial, req->file); + if (req.action == ACTION_INSTALL_APK) { + LOGI("Installing %s...", req.file); + process = install_apk(file_handler->serial, req.file); } else { - LOGI("Pushing %s...", req->file); - process = push_file(file_handler->serial, req->file); + LOGI("Pushing %s...", req.file); + process = push_file(file_handler->serial, req.file); } file_handler->current_process = process; mutex_unlock(file_handler->mutex); - if (req->action == ACTION_INSTALL_APK) { + if (req.action == ACTION_INSTALL_APK) { if (process_check_success(process, "adb install")) { - LOGI("%s successfully installed", req->file); + LOGI("%s successfully installed", req.file); } else { - LOGE("Failed to install %s", req->file); + LOGE("Failed to install %s", req.file); } } else { if (process_check_success(process, "adb push")) { - LOGI("%s successfully pushed to /sdcard/", req->file); + LOGI("%s successfully pushed to /sdcard/", req.file); } else { - LOGE("Failed to push %s to /sdcard/", req->file); + LOGE("Failed to push %s to /sdcard/", req.file); } } - request_free(req); + file_handler_request_destroy(&req); } return 0; } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 382477d8..22245105 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -5,21 +5,21 @@ #include #include +#include "cbuf.h" #include "command.h" -#define REQUEST_QUEUE_SIZE 16 - typedef enum { ACTION_INSTALL_APK, ACTION_PUSH_FILE, } file_handler_action_t; -struct request_queue { - struct request *reqs[REQUEST_QUEUE_SIZE]; - int tail; - int head; +struct file_handler_request { + file_handler_action_t action; + char *file; }; +struct file_handler_request_queue CBUF(struct file_handler_request, 16); + struct file_handler { char *serial; SDL_Thread *thread; @@ -28,7 +28,7 @@ struct file_handler { bool stopped; bool initialized; process_t current_process; - struct request_queue queue; + struct file_handler_request_queue queue; }; bool From 6edb1294f015ee5bf64d6a732c7897bdea79d038 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2019 21:58:07 +0200 Subject: [PATCH 23/90] Add missing return 0 in unit test --- app/tests/test_control_event_serialize.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index 9fae75dc..b8590cba 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -139,4 +139,5 @@ int main(void) { test_serialize_long_text_event(); test_serialize_mouse_event(); test_serialize_scroll_event(); + return 0; } From 5a431cdf9bf3e51e3be7dc2685ce67fc065a495b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 May 2019 13:41:19 +0200 Subject: [PATCH 24/90] Make server_connect_to() return a bool The resulting socket is accessible from the server instance, there is no need to return it. This paves the way to use several sockets in parallel. --- app/src/scrcpy.c | 5 +++-- app/src/server.c | 6 +++--- app/src/server.h | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4a0c045c..776b4c0f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -290,13 +290,14 @@ scrcpy(const struct scrcpy_options *options) { goto finally_destroy_server; } - socket_t device_socket = server_connect_to(&server); - if (device_socket == INVALID_SOCKET) { + if (!server_connect_to(&server)) { server_stop(&server); ret = false; goto finally_destroy_server; } + socket_t device_socket = server.device_socket; + char device_name[DEVICE_NAME_FIELD_LENGTH]; struct size frame_size; diff --git a/app/src/server.c b/app/src/server.c index c37e0070..0d92cabc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -219,7 +219,7 @@ server_start(struct server *server, const char *serial, return true; } -socket_t +bool server_connect_to(struct server *server) { if (!server->tunnel_forward) { server->device_socket = net_accept(server->server_socket); @@ -231,7 +231,7 @@ server_connect_to(struct server *server) { } if (server->device_socket == INVALID_SOCKET) { - return INVALID_SOCKET; + return false; } if (!server->tunnel_forward) { @@ -243,7 +243,7 @@ server_connect_to(struct server *server) { disable_tunnel(server); // ignore failure server->tunnel_enabled = false; - return server->device_socket; + return true; } void diff --git a/app/src/server.h b/app/src/server.h index c72dbdd6..4d16fdab 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -40,7 +40,7 @@ server_start(struct server *server, const char *serial, const char *crop, bool send_frame_meta); // block until the communication with the server is established -socket_t +bool server_connect_to(struct server *server); // disconnect and kill the server process From 8fc58bde75f775b25aa699e4dbf3b1a6049300f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 May 2019 21:02:57 +0200 Subject: [PATCH 25/90] Simplify server_connect_to() Only use 2 branches, using either forward or remote tunnel. --- app/src/server.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 0d92cabc..8760362b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -223,20 +223,20 @@ bool server_connect_to(struct server *server) { if (!server->tunnel_forward) { server->device_socket = net_accept(server->server_socket); + if (server->device_socket == INVALID_SOCKET) { + return false; + } + + // we don't need the server socket anymore + close_socket(&server->server_socket); } else { uint32_t attempts = 100; uint32_t delay = 100; // ms server->device_socket = connect_to_server(server->local_port, attempts, delay); - } - - if (server->device_socket == INVALID_SOCKET) { - return false; - } - - if (!server->tunnel_forward) { - // we don't need the server socket anymore - close_socket(&server->server_socket); + if (server->device_socket == INVALID_SOCKET) { + return false; + } } // we don't need the adb tunnel anymore From 0dee9b04b2866e6e71f2844b58c416bc59741ca0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 May 2019 21:03:44 +0200 Subject: [PATCH 26/90] Use net_recv() to read only one byte Partial read is impossible for 1 byte, so net_recv_all() is useless. --- app/src/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 8760362b..87a61fb7 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -119,7 +119,7 @@ connect_and_read_byte(uint16_t port) { 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) { + if (net_recv(socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel return INVALID_SOCKET; } From bfb86ca2c23c29cf768c9216dbfff3a89d5d0b5f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 11:18:54 +0200 Subject: [PATCH 27/90] Simplify cleanup The cleanup is not linear: for example, the server must be stopped and its sockets must be shutdown after the stream and controller are stopped (so that they don't continue processing garbage), but before they are joined, to avoid a deadlock if they are blocked on a socket read. Simplify the spaghetti-cleanup by keeping trace of initialization at runtime. --- app/src/scrcpy.c | 99 ++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 776b4c0f..539cd940 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -280,20 +280,24 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = false; } - bool ret = true; + bool ret = false; + + bool video_buffer_initialized = false; + bool file_handler_initialized = false; + bool recorder_initialized = false; + bool stream_started = false; + bool controller_initialized = false; + bool controller_started = false; bool display = !options->no_display; bool control = !options->no_control; if (!sdl_init_and_configure(display)) { - ret = false; - goto finally_destroy_server; + goto end; } if (!server_connect_to(&server)) { - server_stop(&server); - ret = false; - goto finally_destroy_server; + goto end; } socket_t device_socket = server.device_socket; @@ -305,23 +309,21 @@ scrcpy(const struct scrcpy_options *options) { // change 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); - ret = false; - goto finally_destroy_server; + goto end; } struct decoder *dec = NULL; if (display) { if (!video_buffer_init(&video_buffer)) { - server_stop(&server); - ret = false; - goto finally_destroy_server; + goto end; } + video_buffer_initialized = true; - if (control && !file_handler_init(&file_handler, server.serial)) { - ret = false; - server_stop(&server); - goto finally_destroy_video_buffer; + if (control) { + if (!file_handler_init(&file_handler, server.serial)) { + goto end; + } + file_handler_initialized = true; } decoder_init(&decoder, &video_buffer); @@ -334,11 +336,10 @@ scrcpy(const struct scrcpy_options *options) { options->record_filename, options->record_format, frame_size)) { - ret = false; - server_stop(&server); - goto finally_destroy_file_handler; + goto end; } rec = &recorder; + recorder_initialized = true; } av_log_set_callback(av_log_callback); @@ -348,28 +349,24 @@ scrcpy(const struct scrcpy_options *options) { // now we consumed the header values, the socket receives the video stream // start the stream if (!stream_start(&stream)) { - ret = false; - server_stop(&server); - goto finally_destroy_recorder; + goto end; } + stream_started = true; if (display) { if (control) { if (!controller_init(&controller, device_socket)) { - ret = false; - goto finally_stop_stream; + goto end; } if (!controller_start(&controller)) { - ret = false; - goto finally_destroy_controller; + goto end; } } if (!screen_init_rendering(&screen, device_name, frame_size, options->always_on_top)) { - ret = false; - goto finally_stop_and_join_controller; + goto end; } if (options->fullscreen) { @@ -387,35 +384,47 @@ scrcpy(const struct scrcpy_options *options) { screen_destroy(&screen); -finally_stop_and_join_controller: - if (display && control) { +end: + // stop stream and controller so that they don't continue once their socket + // is shutdown + if (stream_started) { + stream_stop(&stream); + } + if (controller_started) { controller_stop(&controller); + } + if (file_handler_initialized) { + file_handler_stop(&file_handler); + } + + // shutdown the sockets and kill the server + server_stop(&server); + + // now that the sockets are shutdown, the stream and controller are + // interrupted, we can join them + if (stream_started) { + stream_join(&stream); + } + if (controller_started) { controller_join(&controller); } -finally_destroy_controller: - if (display && control) { + if (controller_initialized) { controller_destroy(&controller); } -finally_stop_stream: - stream_stop(&stream); - // stop the server before stream_join() to wake up the stream - server_stop(&server); - stream_join(&stream); -finally_destroy_recorder: - if (record) { + + if (recorder_initialized) { recorder_destroy(&recorder); } -finally_destroy_file_handler: - if (display && control) { - file_handler_stop(&file_handler); + + if (file_handler_initialized) { file_handler_join(&file_handler); file_handler_destroy(&file_handler); } -finally_destroy_video_buffer: - if (display) { + + if (video_buffer_initialized) { video_buffer_destroy(&video_buffer); } -finally_destroy_server: + if (options->show_touches) { if (!show_touches_waited) { // wait the process which enabled "show touches" From 47f1003200705a237d04af13525d2890a3f74583 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 11:40:12 +0200 Subject: [PATCH 28/90] Close server socket before killing process The sockets may be closed and shutdown on server_stop(). This will interrupt the stream and controller threads more quickly and gracefully. --- app/src/server.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 87a61fb7..06405918 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -248,6 +248,13 @@ server_connect_to(struct server *server) { void server_stop(struct server *server) { + if (server->server_socket != INVALID_SOCKET) { + close_socket(&server->server_socket); + } + if (server->device_socket != INVALID_SOCKET) { + close_socket(&server->device_socket); + } + SDL_assert(server->process != PROCESS_NONE); if (!cmd_terminate(server->process)) { @@ -265,11 +272,5 @@ server_stop(struct server *server) { void server_destroy(struct server *server) { - if (server->server_socket != INVALID_SOCKET) { - close_socket(&server->server_socket); - } - if (server->device_socket != INVALID_SOCKET) { - close_socket(&server->device_socket); - } SDL_free(server->serial); } From 3b4366e5bfa062d80df9ad455b68c98f7f7482c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 11:57:40 +0200 Subject: [PATCH 29/90] Stop stream immediately on quit If the stream is stopped, av_read_frame() will be woken up and yield a corrupted packet. Do not try to decode or record it. --- app/src/stream.c | 9 +++++++++ app/src/stream.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/app/src/stream.c b/app/src/stream.c index 0e751eb3..9a946375 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -207,6 +207,13 @@ run_stream(void *data) { packet.size = 0; while (!av_read_frame(format_ctx, &packet)) { + if (SDL_AtomicGet(&stream->stopped)) { + // if the stream is stopped, the socket had been shutdown, so the + // last packet is probably corrupted (but not detected as such by + // FFmpeg) and will not be decoded correctly + av_packet_unref(&packet); + goto quit; + } if (stream->decoder && !decoder_push(stream->decoder, &packet)) { av_packet_unref(&packet); goto quit; @@ -259,6 +266,7 @@ stream_init(struct stream *stream, socket_t socket, stream->socket = socket; stream->decoder = decoder, stream->recorder = recorder; + SDL_AtomicSet(&stream->stopped, 0); } bool @@ -275,6 +283,7 @@ stream_start(struct stream *stream) { void stream_stop(struct stream *stream) { + SDL_AtomicSet(&stream->stopped, 1); if (stream->decoder) { decoder_interrupt(stream->decoder); } diff --git a/app/src/stream.h b/app/src/stream.h index d5eda0ac..1ebff1a0 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -3,6 +3,7 @@ #include #include +#include #include #include "net.h" @@ -18,6 +19,7 @@ struct stream { socket_t socket; struct video_buffer *video_buffer; SDL_Thread *thread; + SDL_atomic_t stopped; struct decoder *decoder; struct recorder *recorder; struct receiver_state { From 63909fd10d8bf76ac1d7f72bc0be79eb5b6461b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 16:07:24 +0200 Subject: [PATCH 30/90] Merge commands with other control events Several commands were grouped under the same event type "command", with a separate field to indicate the actual command. Move these commands at the same level as other control events. It will allow to implement commands with arguments. --- app/src/control_event.c | 8 +++--- app/src/control_event.h | 13 +++------- app/src/input_manager.c | 12 +++------ .../com/genymobile/scrcpy/ControlEvent.java | 15 +++++------ .../genymobile/scrcpy/ControlEventReader.java | 15 +++-------- .../genymobile/scrcpy/EventController.java | 26 ++++++------------- 6 files changed, 29 insertions(+), 60 deletions(-) diff --git a/app/src/control_event.c b/app/src/control_event.c index e22337fc..83c52551 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -43,9 +43,11 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll); buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll); return 21; - case CONTROL_EVENT_TYPE_COMMAND: - buf[1] = event->command_event.action; - return 2; + case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON: + case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL: + case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL: + // no additional data + return 1; default: LOGW("Unknown event type: %u", (unsigned) event->type); return 0; diff --git a/app/src/control_event.h b/app/src/control_event.h index 13533316..a720e7bc 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -17,13 +17,9 @@ enum control_event_type { CONTROL_EVENT_TYPE_TEXT, CONTROL_EVENT_TYPE_MOUSE, CONTROL_EVENT_TYPE_SCROLL, - CONTROL_EVENT_TYPE_COMMAND, -}; - -enum control_event_command { - CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON, - CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL, - CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON, + CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, + CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, }; struct control_event { @@ -47,9 +43,6 @@ struct control_event { int32_t hscroll; int32_t vscroll; } scroll_event; - struct { - enum control_event_command action; - } command_event; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f77d2d26..9567d21f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -99,9 +99,7 @@ action_menu(struct controller *controller, int actions) { 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_BACK_OR_SCREEN_ON; + control_event.type = CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON; if (!controller_push_event(controller, &control_event)) { LOGW("Cannot turn screen on"); @@ -111,9 +109,7 @@ press_back_or_turn_screen_on(struct controller *controller) { static void expand_notification_panel(struct controller *controller) { struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_COMMAND; - control_event.command_event.action = - CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL; + control_event.type = CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL; if (!controller_push_event(controller, &control_event)) { LOGW("Cannot expand notification panel"); @@ -123,9 +119,7 @@ expand_notification_panel(struct controller *controller) { static void collapse_notification_panel(struct controller *controller) { struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_COMMAND; - control_event.command_event.action = - CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL; + control_event.type = CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL; if (!controller_push_event(controller, &control_event)) { LOGW("Cannot collapse notification panel"); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java index 8b0f9e2b..6318c82b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java @@ -9,16 +9,14 @@ public final class ControlEvent { public static final int TYPE_TEXT = 1; public static final int TYPE_MOUSE = 2; public static final int TYPE_SCROLL = 3; - public static final int TYPE_COMMAND = 4; - - public static final int COMMAND_BACK_OR_SCREEN_ON = 0; - public static final int COMMAND_EXPAND_NOTIFICATION_PANEL = 1; - public static final int COMMAND_COLLAPSE_NOTIFICATION_PANEL = 2; + public static final int TYPE_BACK_OR_SCREEN_ON = 4; + public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; + public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or COMMAND_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* private Position position; @@ -62,10 +60,9 @@ public final class ControlEvent { return event; } - public static ControlEvent createCommandControlEvent(int action) { + public static ControlEvent createSimpleControlEvent(int type) { ControlEvent event = new ControlEvent(); - event.type = TYPE_COMMAND; - event.action = action; + event.type = type; return event; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index 28e9503a..0fdf51c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -11,7 +11,6 @@ public class ControlEventReader { private static final int KEYCODE_PAYLOAD_LENGTH = 9; private static final int MOUSE_PAYLOAD_LENGTH = 17; private static final int SCROLL_PAYLOAD_LENGTH = 20; - private static final int COMMAND_PAYLOAD_LENGTH = 1; public static final int TEXT_MAX_LENGTH = 300; private static final int RAW_BUFFER_SIZE = 1024; @@ -64,8 +63,10 @@ public class ControlEventReader { case ControlEvent.TYPE_SCROLL: controlEvent = parseScrollControlEvent(); break; - case ControlEvent.TYPE_COMMAND: - controlEvent = parseCommandControlEvent(); + case ControlEvent.TYPE_BACK_OR_SCREEN_ON: + case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: + case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: + controlEvent = ControlEvent.createSimpleControlEvent(type); break; default: Ln.w("Unknown event type: " + type); @@ -123,14 +124,6 @@ public class ControlEventReader { return ControlEvent.createScrollControlEvent(position, hScroll, vScroll); } - private ControlEvent parseCommandControlEvent() { - if (buffer.remaining() < COMMAND_PAYLOAD_LENGTH) { - return null; - } - int action = toUnsigned(buffer.get()); - return ControlEvent.createCommandControlEvent(action); - } - private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 9885e04b..762d3b7a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -77,8 +77,14 @@ public class EventController { case ControlEvent.TYPE_SCROLL: injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll()); break; - case ControlEvent.TYPE_COMMAND: - executeCommand(controlEvent.getAction()); + case ControlEvent.TYPE_BACK_OR_SCREEN_ON: + pressBackOrTurnScreenOn(); + break; + case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: + device.expandNotificationPanel(); + break; + case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: + device.collapsePanels(); break; default: // do nothing @@ -170,20 +176,4 @@ public class EventController { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; return injectKeycode(keycode); } - - private boolean executeCommand(int action) { - switch (action) { - case ControlEvent.COMMAND_BACK_OR_SCREEN_ON: - return pressBackOrTurnScreenOn(); - case ControlEvent.COMMAND_EXPAND_NOTIFICATION_PANEL: - device.expandNotificationPanel(); - return true; - case ControlEvent.COMMAND_COLLAPSE_NOTIFICATION_PANEL: - device.collapsePanels(); - return true; - default: - Ln.w("Unsupported command: " + action); - } - return false; - } } From ad4c061cd29041b3cc94a93ff0e861fc0a52439c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 21:24:00 +0200 Subject: [PATCH 31/90] Use custom class Point The framework class android.graphics.Point cannot be used in unit tests. Implement our own Point. --- .../java/com/genymobile/scrcpy/Device.java | 5 +- .../genymobile/scrcpy/EventController.java | 5 +- .../java/com/genymobile/scrcpy/Point.java | 47 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Position.java | 2 - 4 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Point.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 60122c5a..57496363 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.os.RemoteException; @@ -107,8 +106,8 @@ public final class Device { } Rect contentRect = screenInfo.getContentRect(); Point point = position.getPoint(); - int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth(); - int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight(); + int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); + int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); return new Point(scaledX, scaledY); } diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 762d3b7a..5bb79de3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.InputManager; -import android.graphics.Point; import android.os.SystemClock; import android.view.InputDevice; import android.view.InputEvent; @@ -43,8 +42,8 @@ public class EventController { private void setPointerCoords(Point point) { MotionEvent.PointerCoords coords = pointerCoords[0]; - coords.x = point.x; - coords.y = point.y; + coords.x = point.getX(); + coords.y = point.getY(); } private void setScroll(int hScroll, int vScroll) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/Point.java new file mode 100644 index 00000000..9ef2db03 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Point.java @@ -0,0 +1,47 @@ +package com.genymobile.scrcpy; + +import java.util.Objects; + +public class Point { + private final int x; + private final int y; + + public Point(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Point point = (Point) o; + return x == point.x + && y == point.y; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + + @Override + public String toString() { + return "Point{" + + "x=" + x + + ", y=" + y + + '}'; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index e00a6355..757fa36e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy; -import android.graphics.Point; - import java.util.Objects; public class Position { From 63207d9cd589352da3898a33ee0fd88018735838 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 21:46:25 +0200 Subject: [PATCH 32/90] Fix wrong comment in unit test --- app/tests/test_control_event_serialize.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index b8590cba..0818ea50 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -39,7 +39,7 @@ static void test_serialize_text_event(void) { assert(size == 16); const unsigned char expected[] = { - 0x01, // CONTROL_EVENT_TYPE_KEYCODE + 0x01, // CONTROL_EVENT_TYPE_TEXT 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; From 3aa5426cad132792321c2cd370b9f9c156f7e901 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 21:27:41 +0200 Subject: [PATCH 33/90] Add unit tests for control events serialization Add missing tests for serialization and deserialization of control events. --- app/tests/test_control_event_serialize.c | 48 ++++++++++++ .../scrcpy/ControlEventReaderTest.java | 77 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index 0818ea50..6a7720bc 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -133,11 +133,59 @@ static void test_serialize_scroll_event(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_back_or_screen_on_event(void) { + struct control_event event = { + .type = CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON, + }; + + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 1); + + const unsigned char expected[] = { + 0x04, // CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_expand_notification_panel_event(void) { + struct control_event event = { + .type = CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, + }; + + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 1); + + const unsigned char expected[] = { + 0x05, // CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_collapse_notification_panel_event(void) { + struct control_event event = { + .type = CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, + }; + + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 1); + + const unsigned char expected[] = { + 0x06, // CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_keycode_event(); test_serialize_text_event(); test_serialize_long_text_event(); test_serialize_mouse_event(); test_serialize_scroll_event(); + test_serialize_back_or_screen_on_event(); + test_serialize_expand_notification_panel_event(); + test_serialize_collapse_notification_panel_event(); return 0; } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java index 3e97096f..8f0724ff 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java @@ -97,6 +97,83 @@ public class ControlEventReaderTest { Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testParseScrollEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_SCROLL); + dos.writeInt(260); + dos.writeInt(1026); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeInt(1); + dos.writeInt(-1); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_SCROLL, event.getType()); + Assert.assertEquals(260, event.getPosition().getPoint().getX()); + Assert.assertEquals(1026, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1, event.getHScroll()); + Assert.assertEquals(-1, event.getVScroll()); + } + + @Test + public void testParseBackOrScreenOnEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_BACK_OR_SCREEN_ON); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_BACK_OR_SCREEN_ON, event.getType()); + } + + @Test + public void testParseExpandNotificationPanelEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + } + + @Test + public void testParseCollapseNotificationPanelEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlEventReader reader = new ControlEventReader(); From 0a7fe7ad57ac815f0f45b69f8f734e402abb6f41 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 19:01:08 +0200 Subject: [PATCH 34/90] Add helpers to truncate UTF-8 at code points This will help to avoid truncating a UTF-8 string in the middle of a code point, producing an invalid UTF-8 result. --- app/meson.build | 3 +- app/src/str_util.c | 16 +++++++ app/src/str_util.h | 4 ++ app/tests/test_strutil.c | 32 ++++++++++++++ .../com/genymobile/scrcpy/StringUtils.java | 23 ++++++++++ .../genymobile/scrcpy/StringUtilsTest.java | 44 +++++++++++++++++++ 6 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/StringUtils.java create mode 100644 server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java diff --git a/app/meson.build b/app/meson.build index 9bcaa9ae..c6c91e71 100644 --- a/app/meson.build +++ b/app/meson.build @@ -159,7 +159,8 @@ tests = [ ]], ['test_control_event_serialize', [ 'tests/test_control_event_serialize.c', - 'src/control_event.c' + 'src/control_event.c', + 'src/str_util.c' ]], ['test_strutil', [ 'tests/test_strutil.c', diff --git a/app/src/str_util.c b/app/src/str_util.c index d9ae6948..2878bf96 100644 --- a/app/src/str_util.c +++ b/app/src/str_util.c @@ -58,6 +58,22 @@ strquote(const char *src) { return quoted; } +size_t +utf8_truncation_index(const char *utf8, size_t max_len) { + size_t len = strlen(utf8); + if (len <= max_len) { + return len; + } + len = max_len; + // see UTF-8 encoding + while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { + // the next byte is not the start of a new UTF-8 codepoint + // so if we would cut there, the character would be truncated + len--; + } + return len; +} + #ifdef _WIN32 wchar_t * diff --git a/app/src/str_util.h b/app/src/str_util.h index 9ef06cbf..0d1b9c01 100644 --- a/app/src/str_util.h +++ b/app/src/str_util.h @@ -23,6 +23,10 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); char * strquote(const char *src); +// return the index to truncate a UTF-8 string at a valid position +size_t +utf8_truncation_index(const char *utf8, size_t max_len); + #ifdef _WIN32 // convert a UTF-8 string to a wchar_t string // returns the new allocated string, to be freed by the caller diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 1dd7fbbe..18ac4a7d 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -126,6 +126,37 @@ static void test_xstrjoin_truncated_after_sep(void) { assert(!strcmp("abc de ", s)); } +static void test_utf8_truncate(void) { + const char *s = "aÉbÔc"; + assert(strlen(s) == 7); // É and Ô are 2 bytes-wide + + size_t count; + + count = utf8_truncation_index(s, 1); + assert(count == 1); + + count = utf8_truncation_index(s, 2); + assert(count == 1); // É is 2 bytes-wide + + count = utf8_truncation_index(s, 3); + assert(count == 3); + + count = utf8_truncation_index(s, 4); + assert(count == 4); + + count = utf8_truncation_index(s, 5); + assert(count == 4); // Ô is 2 bytes-wide + + count = utf8_truncation_index(s, 6); + assert(count == 6); + + count = utf8_truncation_index(s, 7); + assert(count == 7); + + count = utf8_truncation_index(s, 8); + assert(count == 7); // no more chars +} + int main(void) { test_xstrncpy_simple(); test_xstrncpy_just_fit(); @@ -135,5 +166,6 @@ int main(void) { test_xstrjoin_truncated_in_token(); test_xstrjoin_truncated_before_sep(); test_xstrjoin_truncated_after_sep(); + test_utf8_truncate(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java new file mode 100644 index 00000000..199fc8c1 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +public final class StringUtils { + private StringUtils() { + // not instantiable + } + + @SuppressWarnings("checkstyle:MagicNumber") + public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { + int len = utf8.length; + if (len <= maxLength) { + return len; + } + len = maxLength; + // see UTF-8 encoding + while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { + // the next byte is not the start of a new UTF-8 codepoint + // so if we would cut there, the character would be truncated + len--; + } + return len; + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java new file mode 100644 index 00000000..a2683945 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -0,0 +1,44 @@ +package com.genymobile.scrcpy; + +import junit.framework.Assert; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +public class StringUtilsTest { + + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testUtf8Trucate() { + String s = "aÉbÔc"; + byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); + Assert.assertEquals(7, utf8.length); + + int count; + + count = StringUtils.getUtf8TruncationIndex(utf8, 1); + Assert.assertEquals(1, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 2); + Assert.assertEquals(1, count); // É is 2 bytes-wide + + count = StringUtils.getUtf8TruncationIndex(utf8, 3); + Assert.assertEquals(3, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 4); + Assert.assertEquals(4, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 5); + Assert.assertEquals(4, count); // Ô is 2 bytes-wide + + count = StringUtils.getUtf8TruncationIndex(utf8, 6); + Assert.assertEquals(6, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 7); + Assert.assertEquals(7, count); + + count = StringUtils.getUtf8TruncationIndex(utf8, 8); + Assert.assertEquals(7, count); // no more chars + } +} From 6ec2ddd2d1498db754a10c4c95ea046a5488fe14 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 19:13:19 +0200 Subject: [PATCH 35/90] Truncate UTF-8 properly This will avoid to produce invalid UTF-8 results (although unlikely). --- app/src/control_event.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/control_event.c b/app/src/control_event.c index 83c52551..90eb276c 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -4,6 +4,7 @@ #include "buffer_util.h" #include "log.h" +#include "str_util.h" static void write_position(uint8_t *buf, const struct position *position) { @@ -24,11 +25,10 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { return 10; case CONTROL_EVENT_TYPE_TEXT: { // write length (2 bytes) + string (non nul-terminated) - size_t len = strlen(event->text_event.text); - if (len > CONTROL_EVENT_TEXT_MAX_LENGTH) { - // injecting a text takes time, so limit the text length - len = CONTROL_EVENT_TEXT_MAX_LENGTH; - } + + // injecting a text takes time, so limit the text length + size_t len = utf8_truncation_index(event->text_event.text, + CONTROL_EVENT_TEXT_MAX_LENGTH); buffer_write16be(&buf[1], (uint16_t) len); memcpy(&buf[3], event->text_event.text, len); return 3 + len; From 69360c7407c6e063a4d11aa259fd490c2324a0d5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 18:50:01 +0200 Subject: [PATCH 36/90] Extract control event string serialization A string is serialized as a length (2 bytes) followed by the string data (non nul-terminated). For now, it is used only once, but we will need to serialize strings in other events. --- app/src/control_event.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/control_event.c b/app/src/control_event.c index 90eb276c..80326c58 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -14,6 +14,15 @@ write_position(uint8_t *buf, const struct position *position) { buffer_write16be(&buf[10], position->screen_size.height); } +// write length (2 bytes) + string (non nul-terminated) +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; +} + size_t control_event_serialize(const struct control_event *event, unsigned char *buf) { buf[0] = event->type; @@ -24,14 +33,9 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { buffer_write32be(&buf[6], event->keycode_event.metastate); return 10; case CONTROL_EVENT_TYPE_TEXT: { - // write length (2 bytes) + string (non nul-terminated) - - // injecting a text takes time, so limit the text length - size_t len = utf8_truncation_index(event->text_event.text, - CONTROL_EVENT_TEXT_MAX_LENGTH); - buffer_write16be(&buf[1], (uint16_t) len); - memcpy(&buf[3], event->text_event.text, len); - return 3 + len; + size_t len = write_string(event->text_event.text, + CONTROL_EVENT_TEXT_MAX_LENGTH, &buf[1]); + return 1 + len; } case CONTROL_EVENT_TYPE_MOUSE: buf[1] = event->mouse_event.action; From ec71a3f66ab48c2fdd1728753acc09edbd4db570 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 May 2019 21:03:54 +0200 Subject: [PATCH 37/90] Use two sockets for video and control The socket used the device-to-computer direction to stream the video and the computer-to-device direction to send control events. Some features, like copy-paste from device to computer, require to send non-video data from the device to the computer. To make them possible, use two sockets: - one for streaming the video from the device to the client; - one for control/events in both directions. --- app/src/controller.c | 6 +- app/src/controller.h | 4 +- app/src/scrcpy.c | 8 +- app/src/server.c | 30 ++++++-- app/src/server.h | 6 +- .../genymobile/scrcpy/DesktopConnection.java | 74 +++++++++++-------- .../java/com/genymobile/scrcpy/Server.java | 2 +- 7 files changed, 81 insertions(+), 49 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 30118218..5e74daba 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -7,7 +7,7 @@ #include "log.h" bool -controller_init(struct controller *controller, socket_t video_socket) { +controller_init(struct controller *controller, socket_t control_socket) { cbuf_init(&controller->queue); if (!(controller->mutex = SDL_CreateMutex())) { @@ -19,7 +19,7 @@ controller_init(struct controller *controller, socket_t video_socket) { return false; } - controller->video_socket = video_socket; + controller->control_socket = control_socket; controller->stopped = false; return true; @@ -57,7 +57,7 @@ process_event(struct controller *controller, if (!length) { return false; } - int w = net_send_all(controller->video_socket, serialized_event, length); + int w = net_send_all(controller->control_socket, serialized_event, length); return w == length; } diff --git a/app/src/controller.h b/app/src/controller.h index 7930bf8a..c006b791 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -12,7 +12,7 @@ struct control_event_queue CBUF(struct control_event, 64); struct controller { - socket_t video_socket; + socket_t control_socket; SDL_Thread *thread; SDL_mutex *mutex; SDL_cond *event_cond; @@ -21,7 +21,7 @@ struct controller { }; bool -controller_init(struct controller *controller, socket_t video_socket); +controller_init(struct controller *controller, socket_t control_socket); void controller_destroy(struct controller *controller); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 539cd940..08dbacc3 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -300,15 +300,13 @@ scrcpy(const struct scrcpy_options *options) { goto end; } - socket_t device_socket = server.device_socket; - char device_name[DEVICE_NAME_FIELD_LENGTH]; struct size frame_size; // screenrecord does not send frames when the screen content does not // change 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)) { + if (!device_read_info(server.video_socket, device_name, &frame_size)) { goto end; } @@ -344,7 +342,7 @@ scrcpy(const struct scrcpy_options *options) { av_log_set_callback(av_log_callback); - stream_init(&stream, device_socket, dec, rec); + stream_init(&stream, server.video_socket, dec, rec); // now we consumed the header values, the socket receives the video stream // start the stream @@ -355,7 +353,7 @@ scrcpy(const struct scrcpy_options *options) { if (display) { if (control) { - if (!controller_init(&controller, device_socket)) { + if (!controller_init(&controller, server.control_socket)) { goto end; } diff --git a/app/src/server.c b/app/src/server.c index 06405918..3f76e5c7 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -222,8 +222,14 @@ server_start(struct server *server, const char *serial, bool server_connect_to(struct server *server) { if (!server->tunnel_forward) { - server->device_socket = net_accept(server->server_socket); - if (server->device_socket == INVALID_SOCKET) { + server->video_socket = net_accept(server->server_socket); + if (server->video_socket == INVALID_SOCKET) { + return false; + } + + server->control_socket = net_accept(server->server_socket); + if (server->control_socket == INVALID_SOCKET) { + // the video_socket will be clean up on destroy return false; } @@ -232,9 +238,16 @@ server_connect_to(struct server *server) { } else { uint32_t attempts = 100; uint32_t delay = 100; // ms - server->device_socket = connect_to_server(server->local_port, attempts, - delay); - if (server->device_socket == INVALID_SOCKET) { + server->video_socket = + connect_to_server(server->local_port, attempts, delay); + if (server->video_socket == INVALID_SOCKET) { + return false; + } + + // we know that the device is listening, we don't need several attempts + server->control_socket = + net_connect(IPV4_LOCALHOST, server->local_port); + if (server->control_socket == INVALID_SOCKET) { return false; } } @@ -251,8 +264,11 @@ server_stop(struct server *server) { if (server->server_socket != INVALID_SOCKET) { close_socket(&server->server_socket); } - if (server->device_socket != INVALID_SOCKET) { - close_socket(&server->device_socket); + if (server->video_socket != INVALID_SOCKET) { + close_socket(&server->video_socket); + } + if (server->control_socket != INVALID_SOCKET) { + close_socket(&server->control_socket); } SDL_assert(server->process != PROCESS_NONE); diff --git a/app/src/server.h b/app/src/server.h index 4d16fdab..0c8443bb 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -11,7 +11,8 @@ struct server { char *serial; process_t process; socket_t server_socket; // only used if !tunnel_forward - socket_t device_socket; + socket_t video_socket; + socket_t control_socket; uint16_t local_port; bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" @@ -22,7 +23,8 @@ struct server { .serial = NULL, \ .process = PROCESS_NONE, \ .server_socket = INVALID_SOCKET, \ - .device_socket = INVALID_SOCKET, \ + .video_socket = INVALID_SOCKET, \ + .control_socket = INVALID_SOCKET, \ .local_port = 0, \ .tunnel_enabled = false, \ .tunnel_forward = false, \ diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index d87a7fd8..3c09e725 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -16,16 +16,20 @@ public final class DesktopConnection implements Closeable { private static final String SOCKET_NAME = "scrcpy"; - private final LocalSocket socket; - private final InputStream inputStream; - private final FileDescriptor fd; + private final LocalSocket videoSocket; + private final FileDescriptor videoFd; + + private final LocalSocket controlSocket; + private final InputStream controlInputStream; + private final ControlEventReader reader = new ControlEventReader(); - private DesktopConnection(LocalSocket socket) throws IOException { - this.socket = socket; - inputStream = socket.getInputStream(); - fd = socket.getFileDescriptor(); + private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { + this.videoSocket = videoSocket; + this.controlSocket = controlSocket; + controlInputStream = controlSocket.getInputStream(); + videoFd = videoSocket.getFileDescriptor(); } private static LocalSocket connect(String abstractName) throws IOException { @@ -34,35 +38,47 @@ public final class DesktopConnection implements Closeable { return localSocket; } - 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; + LocalSocket videoSocket; + LocalSocket controlSocket; if (tunnelForward) { - socket = listenAndAccept(SOCKET_NAME); - // send one byte so the client may read() to detect a connection error - socket.getOutputStream().write(0); + LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); + try { + videoSocket = localServerSocket.accept(); + // send one byte so the client may read() to detect a connection error + videoSocket.getOutputStream().write(0); + try { + controlSocket = localServerSocket.accept(); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } + } finally { + localServerSocket.close(); + } } else { - socket = connect(SOCKET_NAME); + videoSocket = connect(SOCKET_NAME); + try { + controlSocket = connect(SOCKET_NAME); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } } - DesktopConnection connection = new DesktopConnection(socket); + DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket); Size videoSize = device.getScreenInfo().getVideoSize(); connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); return connection; } public void close() throws IOException { - socket.shutdownInput(); - socket.shutdownOutput(); - socket.close(); + videoSocket.shutdownInput(); + videoSocket.shutdownOutput(); + videoSocket.close(); + controlSocket.shutdownInput(); + controlSocket.shutdownOutput(); + controlSocket.close(); } @SuppressWarnings("checkstyle:MagicNumber") @@ -78,17 +94,17 @@ public final class DesktopConnection implements Closeable { buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; - IO.writeFully(fd, buffer, 0, buffer.length); + IO.writeFully(videoFd, buffer, 0, buffer.length); } - public FileDescriptor getFd() { - return fd; + public FileDescriptor getVideoFd() { + return videoFd; } public ControlEvent receiveControlEvent() throws IOException { ControlEvent event = reader.next(); while (event == null) { - reader.readFrom(inputStream); + reader.readFrom(controlInputStream); event = reader.next(); } return event; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index b782101c..db192dd1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -24,7 +24,7 @@ public final class Server { try { // synchronous - screenEncoder.streamScreen(device, connection.getFd()); + screenEncoder.streamScreen(device, connection.getVideoFd()); } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); From f9d2d99166f2259f05cd348fcde019b9a314d780 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 00:24:26 +0200 Subject: [PATCH 38/90] Add GET_CLIPBOARD device event Add the first device event, used to forward the device clipboard to the computer. --- app/meson.build | 5 ++ app/src/device_event.c | 48 +++++++++++++++++++ app/src/device_event.h | 33 +++++++++++++ app/tests/test_device_event_deserialize.c | 28 +++++++++++ .../com/genymobile/scrcpy/DeviceEvent.java | 27 +++++++++++ 5 files changed, 141 insertions(+) create mode 100644 app/src/device_event.c create mode 100644 app/src/device_event.h create mode 100644 app/tests/test_device_event_deserialize.c create mode 100644 server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java diff --git a/app/meson.build b/app/meson.build index c6c91e71..326a6656 100644 --- a/app/meson.build +++ b/app/meson.build @@ -6,6 +6,7 @@ src = [ 'src/convert.c', 'src/decoder.c', 'src/device.c', + 'src/device_event.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', @@ -162,6 +163,10 @@ tests = [ 'src/control_event.c', 'src/str_util.c' ]], + ['test_device_event_deserialize', [ + 'tests/test_device_event_deserialize.c', + 'src/device_event.c' + ]], ['test_strutil', [ 'tests/test_strutil.c', 'src/str_util.c' diff --git a/app/src/device_event.c b/app/src/device_event.c new file mode 100644 index 00000000..5bc70d99 --- /dev/null +++ b/app/src/device_event.c @@ -0,0 +1,48 @@ +#include "device_event.h" + +#include +#include + +#include "buffer_util.h" +#include "log.h" + +ssize_t +device_event_deserialize(const unsigned char *buf, size_t len, + struct device_event *event) { + if (len < 3) { + // at least type + empty string length + return 0; // not available + } + + event->type = buf[0]; + switch (event->type) { + case DEVICE_EVENT_TYPE_GET_CLIPBOARD: { + uint16_t clipboard_len = buffer_read16be(&buf[1]); + if (clipboard_len > len - 3) { + return 0; // not available + } + char *text = SDL_malloc(clipboard_len + 1); + if (!text) { + LOGW("Could not allocate text for clipboard"); + return -1; + } + if (clipboard_len) { + memcpy(text, &buf[3], clipboard_len); + } + text[clipboard_len] = '\0'; + + event->clipboard_event.text = text; + return 3 + clipboard_len; + } + default: + LOGW("Unsupported device event type: %d", (int) event->type); + return -1; // error, we cannot recover + } +} + +void +device_event_destroy(struct device_event *event) { + if (event->type == DEVICE_EVENT_TYPE_GET_CLIPBOARD) { + SDL_free(event->clipboard_event.text); + } +} diff --git a/app/src/device_event.h b/app/src/device_event.h new file mode 100644 index 00000000..8e0e8e7f --- /dev/null +++ b/app/src/device_event.h @@ -0,0 +1,33 @@ +#ifndef DEVICEEVENT_H +#define DEVICEEVENT_H + +#include +#include +#include + +#define DEVICE_EVENT_QUEUE_SIZE 64 +#define DEVICE_EVENT_TEXT_MAX_LENGTH 4093 +#define DEVICE_EVENT_SERIALIZED_MAX_SIZE (3 + DEVICE_EVENT_TEXT_MAX_LENGTH) + +enum device_event_type { + DEVICE_EVENT_TYPE_GET_CLIPBOARD, +}; + +struct device_event { + enum device_event_type type; + union { + struct { + char *text; // owned, to be freed by SDL_free() + } clipboard_event; + }; +}; + +// return the number of bytes consumed (0 for no event available, -1 on error) +ssize_t +device_event_deserialize(const unsigned char *buf, size_t len, + struct device_event *event); + +void +device_event_destroy(struct device_event *event); + +#endif diff --git a/app/tests/test_device_event_deserialize.c b/app/tests/test_device_event_deserialize.c new file mode 100644 index 00000000..83e5e02e --- /dev/null +++ b/app/tests/test_device_event_deserialize.c @@ -0,0 +1,28 @@ +#include +#include + +#include "device_event.h" + +#include +static void test_deserialize_clipboard_event(void) { + const unsigned char input[] = { + 0x00, // DEVICE_EVENT_TYPE_CLIPBOARD + 0x00, 0x03, // text length + 0x41, 0x42, 0x43, // "ABC" + }; + + struct device_event event; + ssize_t r = device_event_deserialize(input, sizeof(input), &event); + assert(r == 6); + + assert(event.type == DEVICE_EVENT_TYPE_GET_CLIPBOARD); + assert(event.clipboard_event.text); + assert(!strcmp("ABC", event.clipboard_event.text)); + + device_event_destroy(&event); +} + +int main(void) { + test_deserialize_clipboard_event(); + return 0; +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java b/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java new file mode 100644 index 00000000..97bcbfc6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java @@ -0,0 +1,27 @@ +package com.genymobile.scrcpy; + +public final class DeviceEvent { + + public static final int TYPE_GET_CLIPBOARD = 0; + + private int type; + private String text; + + private DeviceEvent() { + } + + public static DeviceEvent createGetClipboardEvent(String text) { + DeviceEvent event = new DeviceEvent(); + event.type = TYPE_GET_CLIPBOARD; + event.text = text; + return event; + } + + public int getType() { + return type; + } + + public String getText() { + return text; + } +} From 6112095e756cc93a2ba16e81c228a876ae66d07d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 00:25:37 +0200 Subject: [PATCH 39/90] Add device event receiver Create a separate component to handle device events, managed by the controller. --- app/meson.build | 1 + app/src/controller.c | 15 ++++++ app/src/controller.h | 2 + app/src/receiver.c | 108 +++++++++++++++++++++++++++++++++++++++++++ app/src/receiver.h | 32 +++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 app/src/receiver.c create mode 100644 app/src/receiver.h diff --git a/app/meson.build b/app/meson.build index 326a6656..5e832537 100644 --- a/app/meson.build +++ b/app/meson.build @@ -12,6 +12,7 @@ src = [ 'src/input_manager.c', 'src/lock_util.c', 'src/net.c', + 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', diff --git a/app/src/controller.c b/app/src/controller.c index 5e74daba..53ea2e26 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -10,11 +10,17 @@ bool controller_init(struct controller *controller, socket_t control_socket) { cbuf_init(&controller->queue); + if (!receiver_init(&controller->receiver, control_socket)) { + return false; + } + if (!(controller->mutex = SDL_CreateMutex())) { + receiver_destroy(&controller->receiver); return false; } if (!(controller->event_cond = SDL_CreateCond())) { + receiver_destroy(&controller->receiver); SDL_DestroyMutex(controller->mutex); return false; } @@ -34,6 +40,8 @@ controller_destroy(struct controller *controller) { while (cbuf_take(&controller->queue, &event)) { control_event_destroy(&event); } + + receiver_destroy(&controller->receiver); } bool @@ -101,6 +109,12 @@ controller_start(struct controller *controller) { return false; } + if (!receiver_start(&controller->receiver)) { + controller_stop(controller); + SDL_WaitThread(controller->thread, NULL); + return false; + } + return true; } @@ -115,4 +129,5 @@ controller_stop(struct controller *controller) { void controller_join(struct controller *controller) { SDL_WaitThread(controller->thread, NULL); + receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index c006b791..a54859a7 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -8,6 +8,7 @@ #include "cbuf.h" #include "control_event.h" #include "net.h" +#include "receiver.h" struct control_event_queue CBUF(struct control_event, 64); @@ -18,6 +19,7 @@ struct controller { SDL_cond *event_cond; bool stopped; struct control_event_queue queue; + struct receiver receiver; }; bool diff --git a/app/src/receiver.c b/app/src/receiver.c new file mode 100644 index 00000000..683b61fa --- /dev/null +++ b/app/src/receiver.c @@ -0,0 +1,108 @@ +#include "receiver.h" + +#include +#include + +#include "config.h" +#include "device_event.h" +#include "events.h" +#include "lock_util.h" +#include "log.h" + +bool +receiver_init(struct receiver *receiver, socket_t control_socket) { + if (!(receiver->mutex = SDL_CreateMutex())) { + return false; + } + receiver->control_socket = control_socket; + return true; +} + +void +receiver_destroy(struct receiver *receiver) { + SDL_DestroyMutex(receiver->mutex); +} + +static void +process_event(struct receiver *receiver, struct device_event *event) { + switch (event->type) { + case DEVICE_EVENT_TYPE_GET_CLIPBOARD: + SDL_SetClipboardText(event->clipboard_event.text); + break; + } +} + +static ssize_t +process_events(struct receiver *receiver, const unsigned char *buf, + size_t len) { + size_t head = 0; + for (;;) { + struct device_event event; + ssize_t r = device_event_deserialize(&buf[head], len - head, &event); + if (r == -1) { + return -1; + } + if (r == 0) { + return head; + } + + process_event(receiver, &event); + device_event_destroy(&event); + + head += r; + SDL_assert(head <= len); + if (head == len) { + return head; + } + } +} + +static int +run_receiver(void *data) { + struct receiver *receiver = data; + + unsigned char buf[DEVICE_EVENT_SERIALIZED_MAX_SIZE]; + size_t head = 0; + + for (;;) { + SDL_assert(head < DEVICE_EVENT_SERIALIZED_MAX_SIZE); + ssize_t r = net_recv(receiver->control_socket, buf, + DEVICE_EVENT_SERIALIZED_MAX_SIZE - head); + if (r <= 0) { + LOGD("Receiver stopped"); + break; + } + + ssize_t consumed = process_events(receiver, buf, r); + if (consumed == -1) { + // an error occurred + break; + } + + if (consumed) { + // shift the remaining data in the buffer + memmove(buf, &buf[consumed], r - consumed); + head = r - consumed; + } + } + + return 0; +} + +bool +receiver_start(struct receiver *receiver) { + LOGD("Starting receiver thread"); + + receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); + if (!receiver->thread) { + LOGC("Could not start receiver thread"); + return false; + } + + return true; +} + +void +receiver_join(struct receiver *receiver) { + SDL_WaitThread(receiver->thread, NULL); +} diff --git a/app/src/receiver.h b/app/src/receiver.h new file mode 100644 index 00000000..c119b827 --- /dev/null +++ b/app/src/receiver.h @@ -0,0 +1,32 @@ +#ifndef RECEIVER_H +#define RECEIVER_H + +#include +#include +#include + +#include "net.h" + +// receive events from the device +// managed by the controller +struct receiver { + socket_t control_socket; + SDL_Thread *thread; + SDL_mutex *mutex; +}; + +bool +receiver_init(struct receiver *receiver, socket_t control_socket); + +void +receiver_destroy(struct receiver *receiver); + +bool +receiver_start(struct receiver *receiver); + +// no receiver_stop(), it will automatically stop on control_socket shutdown + +void +receiver_join(struct receiver *receiver); + +#endif From 3149e2cf4a9d7b0db34c30d41077a73f048cb919 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 15:17:05 +0200 Subject: [PATCH 40/90] Add device event sender Create a separate component to send device events, managed by the controller. --- .../genymobile/scrcpy/DesktopConnection.java | 9 ++++- .../genymobile/scrcpy/DeviceEventWriter.java | 34 +++++++++++++++++++ .../genymobile/scrcpy/EventController.java | 9 +++-- .../com/genymobile/scrcpy/EventSender.java | 34 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 23 +++++++++++-- 5 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/EventSender.java diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 3c09e725..dcc9863b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -8,6 +8,7 @@ import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; public final class DesktopConnection implements Closeable { @@ -21,14 +22,16 @@ public final class DesktopConnection implements Closeable { private final LocalSocket controlSocket; private final InputStream controlInputStream; - + private final OutputStream controlOutputStream; private final ControlEventReader reader = new ControlEventReader(); + private final DeviceEventWriter writer = new DeviceEventWriter(); private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { this.videoSocket = videoSocket; this.controlSocket = controlSocket; controlInputStream = controlSocket.getInputStream(); + controlOutputStream = controlSocket.getOutputStream(); videoFd = videoSocket.getFileDescriptor(); } @@ -109,4 +112,8 @@ public final class DesktopConnection implements Closeable { } return event; } + + public void sendDeviceEvent(DeviceEvent event) throws IOException { + writer.writeTo(event, controlOutputStream); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java new file mode 100644 index 00000000..e183a221 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class DeviceEventWriter { + + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + + private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; + private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + + @SuppressWarnings("checkstyle:MagicNumber") + public void writeTo(DeviceEvent event, OutputStream output) throws IOException { + buffer.clear(); + buffer.put((byte) DeviceEvent.TYPE_GET_CLIPBOARD); + switch (event.getType()) { + case DeviceEvent.TYPE_GET_CLIPBOARD: + String text = event.getText(); + byte[] raw = text.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); + buffer.putShort((short) len); + buffer.put(raw, 0, len); + output.write(rawBuffer, 0, buffer.position()); + break; + default: + Ln.w("Unknown device event: " + event.getType()); + break; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 5bb79de3..4bda0f3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -11,11 +11,11 @@ import android.view.MotionEvent; import java.io.IOException; - public class EventController { private final Device device; private final DesktopConnection connection; + private final EventSender sender; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -27,6 +27,7 @@ public class EventController { this.device = device; this.connection = connection; initPointer(); + sender = new EventSender(connection); } private void initPointer() { @@ -61,6 +62,10 @@ public class EventController { } } + public EventSender getSender() { + return sender; + } + private void handleEvent() throws IOException { ControlEvent controlEvent = connection.receiveControlEvent(); switch (controlEvent.getType()) { @@ -96,7 +101,7 @@ public class EventController { private boolean injectChar(char c) { String decomposed = KeyComposition.decompose(c); - char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c}; + char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c}; KeyEvent[] events = charMap.getEvents(chars); if (events == null) { return false; diff --git a/server/src/main/java/com/genymobile/scrcpy/EventSender.java b/server/src/main/java/com/genymobile/scrcpy/EventSender.java new file mode 100644 index 00000000..9f50b16a --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/EventSender.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; + +public final class EventSender { + + private final DesktopConnection connection; + + private String clipboardText; + + public EventSender(DesktopConnection connection) { + this.connection = connection; + } + + public synchronized void pushClipboardText(String text) { + clipboardText = text; + notify(); + } + + public void loop() throws IOException, InterruptedException { + while (true) { + String text; + synchronized (this) { + while (clipboardText == null) { + wait(); + } + text = clipboardText; + clipboardText = null; + } + DeviceEvent event = DeviceEvent.createGetClipboardEvent(text); + connection.sendDeviceEvent(event); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index db192dd1..25cb15e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -19,8 +19,11 @@ public final class Server { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); + EventController controller = new EventController(device, connection); + // asynchronous - startEventController(device, connection); + startEventController(controller); + startEventSender(controller.getSender()); try { // synchronous @@ -32,12 +35,12 @@ public final class Server { } } - private static void startEventController(final Device device, final DesktopConnection connection) { + private static void startEventController(final EventController controller) { new Thread(new Runnable() { @Override public void run() { try { - new EventController(device, connection).control(); + controller.control(); } catch (IOException e) { // this is expected on close Ln.d("Event controller stopped"); @@ -46,6 +49,20 @@ public final class Server { }).start(); } + private static void startEventSender(final EventSender sender) { + new Thread(new Runnable() { + @Override + public void run() { + try { + sender.loop(); + } catch (IOException | InterruptedException e) { + // this is expected on close + Ln.d("Event sender stopped"); + } + } + }).start(); + } + @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length != 5) { From 63c078ee6ca19c4a2b3d84b04068220ef425aaf4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 15:23:01 +0200 Subject: [PATCH 41/90] Implement device-to-computer clipboard copy On Ctrl+C: - the client sends a GET_CLIPBOARD command to the device; - the device retrieve its current clipboard text and sends it in a GET_CLIPBOARD device event; - the client sets this text as the system clipboard text, so that it can be pasted in another application. Fixes --- README.md | 1 + app/src/control_event.c | 1 + app/src/control_event.h | 1 + app/src/input_manager.c | 16 +++++++++ app/src/main.c | 3 ++ app/tests/test_control_event_serialize.c | 16 +++++++++ .../com/genymobile/scrcpy/ControlEvent.java | 1 + .../genymobile/scrcpy/ControlEventReader.java | 1 + .../java/com/genymobile/scrcpy/Device.java | 8 +++++ .../genymobile/scrcpy/EventController.java | 4 +++ .../scrcpy/wrappers/ClipboardManager.java | 33 +++++++++++++++++++ .../scrcpy/wrappers/ServiceManager.java | 8 +++++ .../scrcpy/ControlEventReaderTest.java | 16 +++++++++ 13 files changed, 109 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java diff --git a/README.md b/README.md index 5daafb2e..cef20b10 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ you are interested, see [issue 14]. | turn screen on | _Right-click²_ | | expand notification panel | `Ctrl`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` | + | copy device clipboard to computer | `Ctrl`+`c` | | paste computer clipboard to device | `Ctrl`+`v` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | diff --git a/app/src/control_event.c b/app/src/control_event.c index 80326c58..bd42ddb1 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -50,6 +50,7 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON: case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_EVENT_TYPE_GET_CLIPBOARD: // no additional data return 1; default: diff --git a/app/src/control_event.h b/app/src/control_event.h index a720e7bc..cc5afa70 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -20,6 +20,7 @@ enum control_event_type { CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON, CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_EVENT_TYPE_GET_CLIPBOARD, }; struct control_event { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9567d21f..3940f4f8 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -126,6 +126,16 @@ collapse_notification_panel(struct controller *controller) { } } +static void +request_device_clipboard(struct controller *controller) { + struct control_event control_event; + control_event.type = CONTROL_EVENT_TYPE_GET_CLIPBOARD; + + if (!controller_push_event(controller, &control_event)) { + LOGW("Cannot get device clipboard"); + } +} + static void switch_fps_counter_state(struct video_buffer *vb) { mutex_lock(vb->mutex); @@ -250,6 +260,12 @@ input_manager_process_key(struct input_manager *input_manager, action_volume_up(input_manager->controller, action); } return; + case SDLK_c: + if (control && ctrl && !meta && !shift && !repeat + && event->type == SDL_KEYDOWN) { + request_device_clipboard(input_manager->controller); + } + return; case SDLK_v: if (control && ctrl && !meta && !shift && !repeat && event->type == SDL_KEYDOWN) { diff --git a/app/src/main.c b/app/src/main.c index fe93673f..8b415909 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -137,6 +137,9 @@ static void usage(const char *arg0) { " Ctrl+Shift+n\n" " collapse notification panel\n" "\n" + " Ctrl+c\n" + " copy device clipboard to computer\n" + "\n" " Ctrl+v\n" " paste computer clipboard to device\n" "\n" diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index 6a7720bc..038d913d 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -178,6 +178,21 @@ static void test_serialize_collapse_notification_panel_event(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_get_clipboard_event(void) { + struct control_event event = { + .type = CONTROL_EVENT_TYPE_GET_CLIPBOARD, + }; + + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 1); + + const unsigned char expected[] = { + 0x07, // CONTROL_EVENT_TYPE_GET_CLIPBOARD + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_keycode_event(); test_serialize_text_event(); @@ -187,5 +202,6 @@ int main(void) { test_serialize_back_or_screen_on_event(); test_serialize_expand_notification_panel_event(); test_serialize_collapse_notification_panel_event(); + test_serialize_get_clipboard_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 6318c82b..1784c953 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java @@ -12,6 +12,7 @@ public final class ControlEvent { public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_GET_CLIPBOARD = 7; 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 0fdf51c5..b316c34a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -66,6 +66,7 @@ public class ControlEventReader { case ControlEvent.TYPE_BACK_OR_SCREEN_ON: case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlEvent.TYPE_GET_CLIPBOARD: controlEvent = ControlEvent.createSimpleControlEvent(type); break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 57496363..c373d390 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -139,6 +139,14 @@ public final class Device { serviceManager.getStatusBarManager().collapsePanels(); } + public String getClipboardText() { + CharSequence s = serviceManager.getClipboardManager().getText(); + if (s == null) { + return null; + } + return s.toString(); + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 4bda0f3d..bec25103 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -90,6 +90,10 @@ public class EventController { case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: device.collapsePanels(); break; + case ControlEvent.TYPE_GET_CLIPBOARD: + String clipboardText = device.getClipboardText(); + sender.pushClipboardText(clipboardText); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java new file mode 100644 index 00000000..60ce5f13 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy.wrappers; + +import android.content.ClipData; +import android.os.IInterface; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ClipboardManager { + private final IInterface manager; + private final Method getPrimaryClipMethod; + + public ClipboardManager(IInterface manager) { + this.manager = manager; + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + public CharSequence getText() { + try { + ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + if (clipData == null || clipData.getItemCount() == 0) { + return null; + } + return clipData.getItemAt(0).getText(); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } + } +} 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 3bcdc0e6..0b625c92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -15,6 +15,7 @@ public final class ServiceManager { private InputManager inputManager; private PowerManager powerManager; private StatusBarManager statusBarManager; + private ClipboardManager clipboardManager; public ServiceManager() { try { @@ -68,4 +69,11 @@ public final class ServiceManager { } return statusBarManager; } + + public ClipboardManager getClipboardManager() { + if (clipboardManager == null) { + clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + } + return clipboardManager; + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java index 8f0724ff..d19d1bc6 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java @@ -174,6 +174,22 @@ public class ControlEventReaderTest { Assert.assertEquals(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); } + @Test + public void testParseGetClipboardEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_GET_CLIPBOARD); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_GET_CLIPBOARD, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlEventReader reader = new ControlEventReader(); From 61f5f96b4229a97152bfc05bc97682dd8316d071 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 16:33:13 +0200 Subject: [PATCH 42/90] Fix control event String parsing At least 2 bytes must be available to read the length of the String. --- .../src/main/java/com/genymobile/scrcpy/ControlEventReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index b316c34a..8eeea27c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -93,7 +93,7 @@ public class ControlEventReader { } private ControlEvent parseTextControlEvent() { - if (buffer.remaining() < 1) { + if (buffer.remaining() < 2) { return null; } int len = toUnsigned(buffer.getShort()); From 232206965671f921a1d5a881e2b28f63aff1eb72 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 16:34:37 +0200 Subject: [PATCH 43/90] Extract control event String parsing Parsing a String from a serialized control event, encoded as length (2 bytes) + data, will be necessary in several events. Extract it to a separate method. --- .../com/genymobile/scrcpy/ControlEventReader.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index 8eeea27c..c48b1826 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -92,7 +92,7 @@ public class ControlEventReader { return ControlEvent.createKeycodeControlEvent(action, keycode, metaState); } - private ControlEvent parseTextControlEvent() { + private String parseString() { if (buffer.remaining() < 2) { return null; } @@ -101,7 +101,14 @@ public class ControlEventReader { return null; } buffer.get(textBuffer, 0, len); - String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8); + return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + } + + private ControlEvent parseTextControlEvent() { + String text = parseString(); + if (text == null) { + return null; + } return ControlEvent.createTextControlEvent(text); } From c13a24389cae489e01ba5a31f4ef6b384d1f8547 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 20:25:23 +0200 Subject: [PATCH 44/90] Implement computer-to-device clipboard copy It was already possible to _paste_ (with Ctrl+v) the content of the computer clipboard on the device. Technically, it injects a sequence of events to generate the text. Add a new feature (Ctrl+Shift+v) to copy to the device clipboard instead, without injecting the content. Contrary to events injection, this preserves the UTF-8 content exactly, so the text is not broken by special characters. --- README.md | 1 + app/src/control_event.c | 18 ++++++++-- app/src/control_event.h | 8 ++++- app/src/input_manager.c | 33 +++++++++++++++++-- app/src/main.c | 3 ++ app/tests/test_control_event_serialize.c | 21 ++++++++++++ .../com/genymobile/scrcpy/ControlEvent.java | 8 +++++ .../genymobile/scrcpy/ControlEventReader.java | 14 +++++++- .../java/com/genymobile/scrcpy/Device.java | 4 +++ .../genymobile/scrcpy/EventController.java | 3 ++ .../scrcpy/wrappers/ClipboardManager.java | 11 +++++++ .../scrcpy/ControlEventReaderTest.java | 20 +++++++++++ 12 files changed, 138 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cef20b10..ef1424ba 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ you are interested, see [issue 14]. | collapse notification panel | `Ctrl`+`Shift`+`n` | | copy device clipboard to computer | `Ctrl`+`c` | | paste computer clipboard to device | `Ctrl`+`v` | + | copy computer clipboard to device | `Ctrl`+`Shift+`v` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | _¹Double-click on black borders to remove them._ diff --git a/app/src/control_event.c b/app/src/control_event.c index bd42ddb1..cc4e5b1d 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -47,6 +47,12 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll); buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll); return 21; + case CONTROL_EVENT_TYPE_SET_CLIPBOARD: { + size_t len = write_string(event->text_event.text, + CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH, + &buf[1]); + return 1 + len; + } case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON: case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL: @@ -61,7 +67,15 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { void control_event_destroy(struct control_event *event) { - if (event->type == CONTROL_EVENT_TYPE_TEXT) { - SDL_free(event->text_event.text); + switch (event->type) { + case CONTROL_EVENT_TYPE_TEXT: + SDL_free(event->text_event.text); + break; + case CONTROL_EVENT_TYPE_SET_CLIPBOARD: + SDL_free(event->set_clipboard_event.text); + break; + default: + // do nothing + break; } } diff --git a/app/src/control_event.h b/app/src/control_event.h index cc5afa70..c381af26 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -10,7 +10,9 @@ #include "common.h" #define CONTROL_EVENT_TEXT_MAX_LENGTH 300 -#define CONTROL_EVENT_SERIALIZED_MAX_SIZE (3 + CONTROL_EVENT_TEXT_MAX_LENGTH) +#define CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_EVENT_SERIALIZED_MAX_SIZE \ + (3 + CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH) enum control_event_type { CONTROL_EVENT_TYPE_KEYCODE, @@ -21,6 +23,7 @@ enum control_event_type { CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_EVENT_TYPE_GET_CLIPBOARD, + CONTROL_EVENT_TYPE_SET_CLIPBOARD, }; struct control_event { @@ -44,6 +47,9 @@ struct control_event { int32_t hscroll; int32_t vscroll; } scroll_event; + struct { + char *text; // owned, to be freed by SDL_free() + } set_clipboard_event; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3940f4f8..23073cbc 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -136,6 +136,29 @@ request_device_clipboard(struct controller *controller) { } } +static void +set_device_clipboard(struct controller *controller) { + char *text = SDL_GetClipboardText(); + if (!text) { + LOGW("Cannot get clipboard text: %s", SDL_GetError()); + return; + } + if (!*text) { + // empty text + SDL_free(text); + return; + } + + struct control_event control_event; + control_event.type = CONTROL_EVENT_TYPE_SET_CLIPBOARD; + control_event.set_clipboard_event.text = text; + + if (!controller_push_event(controller, &control_event)) { + SDL_free(text); + LOGW("Cannot send clipboard paste event"); + } +} + static void switch_fps_counter_state(struct video_buffer *vb) { mutex_lock(vb->mutex); @@ -267,9 +290,15 @@ input_manager_process_key(struct input_manager *input_manager, } return; case SDLK_v: - if (control && ctrl && !meta && !shift && !repeat + if (control && ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) { - clipboard_paste(input_manager->controller); + if (shift) { + // store the text in the device clipboard + set_device_clipboard(input_manager->controller); + } else { + // inject the text as input events + clipboard_paste(input_manager->controller); + } } return; case SDLK_f: diff --git a/app/src/main.c b/app/src/main.c index 8b415909..be611025 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -143,6 +143,9 @@ static void usage(const char *arg0) { " Ctrl+v\n" " paste computer clipboard to device\n" "\n" + " Ctrl+Shift+v\n" + " copy computer clipboard to device\n" + "\n" " Ctrl+i\n" " enable/disable FPS counter (print frames/second in logs)\n" "\n" diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index 038d913d..d9cd0b76 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -193,6 +193,26 @@ static void test_serialize_get_clipboard_event(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_set_clipboard_event(void) { + struct control_event event = { + .type = CONTROL_EVENT_TYPE_SET_CLIPBOARD, + .text_event = { + .text = "hello, world!", + }, + }; + + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 16); + + const unsigned char expected[] = { + 0x08, // CONTROL_EVENT_TYPE_SET_CLIPBOARD + 0x00, 0x0d, // text length + 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_keycode_event(); test_serialize_text_event(); @@ -203,5 +223,6 @@ int main(void) { test_serialize_expand_notification_panel_event(); test_serialize_collapse_notification_panel_event(); test_serialize_get_clipboard_event(); + test_serialize_set_clipboard_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 1784c953..51360560 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java @@ -13,6 +13,7 @@ public final class ControlEvent { public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; public static final int TYPE_GET_CLIPBOARD = 7; + public static final int TYPE_SET_CLIPBOARD = 8; private int type; private String text; @@ -61,6 +62,13 @@ public final class ControlEvent { return event; } + public static ControlEvent createSetClipboardControlEvent(String text) { + ControlEvent event = new ControlEvent(); + event.type = TYPE_SET_CLIPBOARD; + event.text = text; + return event; + } + public static ControlEvent createSimpleControlEvent(int type) { ControlEvent event = new ControlEvent(); event.type = type; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index c48b1826..ec807232 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -13,11 +13,12 @@ public class ControlEventReader { private static final int SCROLL_PAYLOAD_LENGTH = 20; public static final int TEXT_MAX_LENGTH = 300; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; private static final int RAW_BUFFER_SIZE = 1024; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH]; + private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; public ControlEventReader() { // invariant: the buffer is always in "get" mode @@ -63,6 +64,9 @@ public class ControlEventReader { case ControlEvent.TYPE_SCROLL: controlEvent = parseScrollControlEvent(); break; + case ControlEvent.TYPE_SET_CLIPBOARD: + controlEvent = parseSetClipboardEvent(); + break; case ControlEvent.TYPE_BACK_OR_SCREEN_ON: case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: @@ -132,6 +136,14 @@ public class ControlEventReader { return ControlEvent.createScrollControlEvent(position, hScroll, vScroll); } + private ControlEvent parseSetClipboardEvent() { + String text = parseString(); + if (text == null) { + return null; + } + return ControlEvent.createSetClipboardControlEvent(text); + } + private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index c373d390..f791266b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -147,6 +147,10 @@ public final class Device { return s.toString(); } + public void setClipboardText(String text) { + serviceManager.getClipboardManager().setText(text); + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index bec25103..e8cd2d68 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -94,6 +94,9 @@ public class EventController { String clipboardText = device.getClipboardText(); sender.pushClipboardText(clipboardText); break; + case ControlEvent.TYPE_SET_CLIPBOARD: + device.setClipboardText(controlEvent.getText()); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 60ce5f13..a058a8bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -9,11 +9,13 @@ import java.lang.reflect.Method; public class ClipboardManager { private final IInterface manager; private final Method getPrimaryClipMethod; + private final Method setPrimaryClipMethod; public ClipboardManager(IInterface manager) { this.manager = manager; try { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); } catch (NoSuchMethodException e) { throw new AssertionError(e); } @@ -30,4 +32,13 @@ public class ClipboardManager { throw new AssertionError(e); } } + + public void setText(CharSequence text) { + ClipData clipData = ClipData.newPlainText(null, text); + try { + setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java index d19d1bc6..692b5d23 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java @@ -190,6 +190,26 @@ public class ControlEventReaderTest { Assert.assertEquals(ControlEvent.TYPE_GET_CLIPBOARD, event.getType()); } + @Test + public void testParseSetClipboardEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_SET_CLIPBOARD); + byte[] text = "testé".getBytes(StandardCharsets.UTF_8); + dos.writeShort(text.length); + dos.write(text); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals("testé", event.getText()); + } + @Test public void testMultiEvents() throws IOException { ControlEventReader reader = new ControlEventReader(); From 0125af1e46ae88c3ef05210686ab55e553ab3e96 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 22:04:20 +0200 Subject: [PATCH 45/90] Update DEVELOP after recent refactorings --- DEVELOP.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index 38c9c63e..ba9e130b 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -76,6 +76,9 @@ The server uses 2 threads: - the **main** thread, encoding and streaming the video to the client; - the **controller** thread, listening for _control events_ (typically, keyboard and mouse events) from the client. + - the **receiver** thread (managed by the controller), sending _device events_ + to the clients (currently, it is only used to send the device clipboard + content). Since the video encoding is typically hardware, there would be no benefit in encoding and streaming in two different threads. @@ -114,12 +117,12 @@ https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobi ### Input events injection _Control events_ are received from the client by the [`EventController`] (run in -a separate thread). There are 5 types of input events: +a separate thread). There are several types of input events: - keycode (cf [`KeyEvent`]), - text (special characters may not be handled by keycodes directly), - mouse motion/click, - mouse scroll, - - custom command (e.g. to switch the screen on). + - other commands (e.g. to switch the screen on or to copy the clipboard). All of them may need to inject input events to the system. To do so, they use the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our @@ -146,8 +149,8 @@ The video stream is decoded by [libav] (FFmpeg). ### Initialization On startup, in addition to _libav_ and _SDL_ initialization, the client must -push and start the server on the device, and open a socket so that they may -communicate. +push and start the server on the device, and open two sockets (one for the video +stream, one for control) so that they may communicate. Note that the client-server roles are expressed at the application level: @@ -180,12 +183,14 @@ the connection from the server (see commit [90a46b4]). ### Threading -The client uses 3 threads: +The client uses 4 threads: - the **main** thread, executing the SDL event loop, - the **stream** thread, receiving the video and used for decoding and recording, - the **controller** thread, sending _control events_ to the server. + - the **receiver** thread (managed by the controller), receiving _device + events_ from the client. In addition, another thread can be started if necessary to handle APK installation or file push requests (via drag&drop on the main window). @@ -235,9 +240,8 @@ in a separate thread, to avoid I/O on the main thread. On SDL event, received on the main thread, the [input manager][inputmanager] creates appropriate [_control events_][controlevent]. It is responsible to convert SDL events to Android events (using [convert]). It pushes the _control -events_ to a blocking queue hold by the controller. On its own thread, the -controller takes events from the queue, that it serializes and sends to the -client. +events_ to a queue hold by the controller. On its own thread, the controller +takes events from the queue, that it serializes and sends to the client. [controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h [controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h From c2cef8d501226688c90f3d7038c799e669158b24 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 25 May 2019 10:56:17 +0800 Subject: [PATCH 46/90] server/meson.build: Prevent using input field for directory This will fix build warning in newer meson. Fix #540. Signed-off-by: Yu-Chen Lin --- server/meson.build | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/meson.build b/server/meson.build index 327e6aca..d96373a0 100644 --- a/server/meson.build +++ b/server/meson.build @@ -4,9 +4,8 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', build_always: true, # gradle is responsible for tracking source changes - input: '.', output: 'scrcpy-server.jar', - command: [find_program('./scripts/build-wrapper.sh'), '@INPUT@', '@OUTPUT@', get_option('buildtype')], + command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], install: true, install_dir: 'share/scrcpy') else From 2a8a3e6ed5f019bdb296dd2fdfa3a2d486b0bc15 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Fri, 31 May 2019 20:57:06 +0800 Subject: [PATCH 47/90] Correct return value type in handle_event handle_event return the type enum event_result not bool Signed-off-by: Yu-Chen Lin --- app/src/scrcpy.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 08dbacc3..6cc0a3ec 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -132,7 +132,7 @@ handle_event(SDL_Event *event, bool control) { screen_show_window(&screen); } if (!screen_update_frame(&screen, &video_buffer)) { - return false; + return EVENT_RESULT_CONTINUE; } break; case SDL_WINDOWEVENT: From 28980bbc90ab93cf61bdc4b36d2448b8cebbb7df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 14:55:11 +0200 Subject: [PATCH 48/90] Rename "event" to "message" After the recent refactorings, a "control event" is not necessarily an "event" (it may be a "command"). Similarly, the unique "device event" used to send the device clipboard content is more a "reponse" to the request from the client than an "event". Rename both to "message", and rename the message types to better describe their intent. --- app/meson.build | 12 +- app/src/control_event.c | 81 ------- app/src/control_event.h | 64 ----- app/src/control_msg.c | 83 +++++++ app/src/control_msg.h | 64 +++++ app/src/controller.c | 44 ++-- app/src/controller.h | 16 +- app/src/convert.c | 49 ++-- app/src/convert.h | 10 +- app/src/device_event.h | 33 --- app/src/{device_event.c => device_msg.c} | 22 +- app/src/device_msg.h | 32 +++ app/src/input_manager.c | 123 +++++----- app/src/receiver.c | 30 ++- app/tests/test_control_event_serialize.c | 228 ------------------ app/tests/test_control_msg_serialize.c | 228 ++++++++++++++++++ app/tests/test_device_event_deserialize.c | 28 --- app/tests/test_device_msg_deserialize.c | 28 +++ ...{ControlEvent.java => ControlMessage.java} | 44 ++-- ...tReader.java => ControlMessageReader.java} | 78 +++--- .../{EventController.java => Controller.java} | 42 ++-- .../genymobile/scrcpy/DesktopConnection.java | 18 +- .../com/genymobile/scrcpy/DeviceEvent.java | 27 --- .../com/genymobile/scrcpy/DeviceMessage.java | 27 +++ ...ntSender.java => DeviceMessageSender.java} | 8 +- ...ntWriter.java => DeviceMessageWriter.java} | 14 +- .../java/com/genymobile/scrcpy/Server.java | 14 +- ...est.java => ControlMessageReaderTest.java} | 108 ++++----- 28 files changed, 777 insertions(+), 778 deletions(-) delete mode 100644 app/src/control_event.c delete mode 100644 app/src/control_event.h create mode 100644 app/src/control_msg.c create mode 100644 app/src/control_msg.h delete mode 100644 app/src/device_event.h rename app/src/{device_event.c => device_msg.c} (61%) create mode 100644 app/src/device_msg.h delete mode 100644 app/tests/test_control_event_serialize.c create mode 100644 app/tests/test_control_msg_serialize.c delete mode 100644 app/tests/test_device_event_deserialize.c create mode 100644 app/tests/test_device_msg_deserialize.c rename server/src/main/java/com/genymobile/scrcpy/{ControlEvent.java => ControlMessage.java} (61%) rename server/src/main/java/com/genymobile/scrcpy/{ControlEventReader.java => ControlMessageReader.java} (61%) rename server/src/main/java/com/genymobile/scrcpy/{EventController.java => Controller.java} (81%) delete mode 100644 server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java rename server/src/main/java/com/genymobile/scrcpy/{EventSender.java => DeviceMessageSender.java} (74%) rename server/src/main/java/com/genymobile/scrcpy/{DeviceEventWriter.java => DeviceMessageWriter.java} (72%) rename server/src/test/java/com/genymobile/scrcpy/{ControlEventReaderTest.java => ControlMessageReaderTest.java} (69%) diff --git a/app/meson.build b/app/meson.build index 5e832537..d47eda79 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,12 +1,12 @@ src = [ 'src/main.c', 'src/command.c', - 'src/control_event.c', + 'src/control_msg.c', 'src/controller.c', 'src/convert.c', 'src/decoder.c', 'src/device.c', - 'src/device_event.c', + 'src/device_msg.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', @@ -160,13 +160,13 @@ tests = [ 'tests/test_cbuf.c', ]], ['test_control_event_serialize', [ - 'tests/test_control_event_serialize.c', - 'src/control_event.c', + 'tests/test_control_msg_serialize.c', + 'src/control_msg.c', 'src/str_util.c' ]], ['test_device_event_deserialize', [ - 'tests/test_device_event_deserialize.c', - 'src/device_event.c' + 'tests/test_device_msg_deserialize.c', + 'src/device_msg.c' ]], ['test_strutil', [ 'tests/test_strutil.c', diff --git a/app/src/control_event.c b/app/src/control_event.c deleted file mode 100644 index cc4e5b1d..00000000 --- a/app/src/control_event.c +++ /dev/null @@ -1,81 +0,0 @@ -#include "control_event.h" - -#include - -#include "buffer_util.h" -#include "log.h" -#include "str_util.h" - -static void -write_position(uint8_t *buf, const struct position *position) { - buffer_write32be(&buf[0], position->point.x); - buffer_write32be(&buf[4], position->point.y); - buffer_write16be(&buf[8], position->screen_size.width); - buffer_write16be(&buf[10], position->screen_size.height); -} - -// write length (2 bytes) + string (non nul-terminated) -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; -} - -size_t -control_event_serialize(const struct control_event *event, unsigned char *buf) { - buf[0] = event->type; - switch (event->type) { - case CONTROL_EVENT_TYPE_KEYCODE: - buf[1] = event->keycode_event.action; - buffer_write32be(&buf[2], event->keycode_event.keycode); - buffer_write32be(&buf[6], event->keycode_event.metastate); - return 10; - case CONTROL_EVENT_TYPE_TEXT: { - size_t len = write_string(event->text_event.text, - CONTROL_EVENT_TEXT_MAX_LENGTH, &buf[1]); - return 1 + len; - } - case CONTROL_EVENT_TYPE_MOUSE: - buf[1] = event->mouse_event.action; - buffer_write32be(&buf[2], event->mouse_event.buttons); - write_position(&buf[6], &event->mouse_event.position); - return 18; - case CONTROL_EVENT_TYPE_SCROLL: - write_position(&buf[1], &event->scroll_event.position); - buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll); - buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll); - return 21; - case CONTROL_EVENT_TYPE_SET_CLIPBOARD: { - size_t len = write_string(event->text_event.text, - CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[1]); - return 1 + len; - } - case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON: - case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL: - case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL: - case CONTROL_EVENT_TYPE_GET_CLIPBOARD: - // no additional data - return 1; - default: - LOGW("Unknown event type: %u", (unsigned) event->type); - return 0; - } -} - -void -control_event_destroy(struct control_event *event) { - switch (event->type) { - case CONTROL_EVENT_TYPE_TEXT: - SDL_free(event->text_event.text); - break; - case CONTROL_EVENT_TYPE_SET_CLIPBOARD: - SDL_free(event->set_clipboard_event.text); - break; - default: - // do nothing - break; - } -} diff --git a/app/src/control_event.h b/app/src/control_event.h deleted file mode 100644 index c381af26..00000000 --- a/app/src/control_event.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef CONTROLEVENT_H -#define CONTROLEVENT_H - -#include -#include -#include - -#include "android/input.h" -#include "android/keycodes.h" -#include "common.h" - -#define CONTROL_EVENT_TEXT_MAX_LENGTH 300 -#define CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH 4093 -#define CONTROL_EVENT_SERIALIZED_MAX_SIZE \ - (3 + CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH) - -enum control_event_type { - CONTROL_EVENT_TYPE_KEYCODE, - CONTROL_EVENT_TYPE_TEXT, - CONTROL_EVENT_TYPE_MOUSE, - CONTROL_EVENT_TYPE_SCROLL, - CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON, - CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, - CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, - CONTROL_EVENT_TYPE_GET_CLIPBOARD, - CONTROL_EVENT_TYPE_SET_CLIPBOARD, -}; - -struct control_event { - enum control_event_type type; - union { - struct { - enum android_keyevent_action action; - enum android_keycode keycode; - enum android_metastate metastate; - } keycode_event; - struct { - char *text; // owned, to be freed by SDL_free() - } text_event; - struct { - enum android_motionevent_action action; - enum android_motionevent_buttons buttons; - struct position position; - } mouse_event; - struct { - struct position position; - int32_t hscroll; - int32_t vscroll; - } scroll_event; - struct { - char *text; // owned, to be freed by SDL_free() - } set_clipboard_event; - }; -}; - -// buf size must be at least CONTROL_EVENT_SERIALIZED_MAX_SIZE -// return the number of bytes written -size_t -control_event_serialize(const struct control_event *event, unsigned char *buf); - -void -control_event_destroy(struct control_event *event); - -#endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c new file mode 100644 index 00000000..ca1028b3 --- /dev/null +++ b/app/src/control_msg.c @@ -0,0 +1,83 @@ +#include "control_msg.h" + +#include + +#include "buffer_util.h" +#include "log.h" +#include "str_util.h" + +static void +write_position(uint8_t *buf, const struct position *position) { + buffer_write32be(&buf[0], position->point.x); + buffer_write32be(&buf[4], position->point.y); + buffer_write16be(&buf[8], position->screen_size.width); + buffer_write16be(&buf[10], position->screen_size.height); +} + +// write length (2 bytes) + string (non nul-terminated) +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; +} + +size_t +control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { + buf[0] = msg->type; + switch (msg->type) { + 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; + case CONTROL_MSG_TYPE_INJECT_TEXT: { + size_t len = write_string(msg->inject_text.text, + CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); + return 1 + len; + } + case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: + buf[1] = msg->inject_mouse_event.action; + buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); + write_position(&buf[6], &msg->inject_mouse_event.position); + return 18; + case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + write_position(&buf[1], &msg->inject_scroll_event.position); + buffer_write32be(&buf[13], + (uint32_t) msg->inject_scroll_event.hscroll); + buffer_write32be(&buf[17], + (uint32_t) msg->inject_scroll_event.vscroll); + return 21; + case CONTROL_MSG_TYPE_SET_CLIPBOARD: { + size_t len = write_string(msg->inject_text.text, + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, + &buf[1]); + return 1 + len; + } + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + // no additional data + return 1; + default: + LOGW("Unknown message type: %u", (unsigned) msg->type); + return 0; + } +} + +void +control_msg_destroy(struct control_msg *msg) { + switch (msg->type) { + case CONTROL_MSG_TYPE_INJECT_TEXT: + SDL_free(msg->inject_text.text); + break; + case CONTROL_MSG_TYPE_SET_CLIPBOARD: + SDL_free(msg->set_clipboard.text); + break; + default: + // do nothing + break; + } +} diff --git a/app/src/control_msg.h b/app/src/control_msg.h new file mode 100644 index 00000000..abc11f32 --- /dev/null +++ b/app/src/control_msg.h @@ -0,0 +1,64 @@ +#ifndef CONTROLMSG_H +#define CONTROLMSG_H + +#include +#include +#include + +#include "android/input.h" +#include "android/keycodes.h" +#include "common.h" + +#define CONTROL_MSG_TEXT_MAX_LENGTH 300 +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ + (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) + +enum control_msg_type { + CONTROL_MSG_TYPE_INJECT_KEYCODE, + CONTROL_MSG_TYPE_INJECT_TEXT, + CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_GET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_CLIPBOARD, +}; + +struct control_msg { + enum control_msg_type type; + union { + struct { + enum android_keyevent_action action; + enum android_keycode keycode; + enum android_metastate metastate; + } inject_keycode; + struct { + char *text; // owned, to be freed by SDL_free() + } inject_text; + struct { + enum android_motionevent_action action; + enum android_motionevent_buttons buttons; + struct position position; + } inject_mouse_event; + struct { + struct position position; + int32_t hscroll; + int32_t vscroll; + } inject_scroll_event; + struct { + char *text; // owned, to be freed by SDL_free() + } set_clipboard; + }; +}; + +// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE +// return the number of bytes written +size_t +control_msg_serialize(const struct control_msg *msg, unsigned char *buf); + +void +control_msg_destroy(struct control_msg *msg); + +#endif diff --git a/app/src/controller.c b/app/src/controller.c index 53ea2e26..4b1f4c8b 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -19,7 +19,7 @@ controller_init(struct controller *controller, socket_t control_socket) { return false; } - if (!(controller->event_cond = SDL_CreateCond())) { + if (!(controller->msg_cond = SDL_CreateCond())) { receiver_destroy(&controller->receiver); SDL_DestroyMutex(controller->mutex); return false; @@ -33,39 +33,39 @@ controller_init(struct controller *controller, socket_t control_socket) { void controller_destroy(struct controller *controller) { - SDL_DestroyCond(controller->event_cond); + SDL_DestroyCond(controller->msg_cond); SDL_DestroyMutex(controller->mutex); - struct control_event event; - while (cbuf_take(&controller->queue, &event)) { - control_event_destroy(&event); + struct control_msg msg; + while (cbuf_take(&controller->queue, &msg)) { + control_msg_destroy(&msg); } receiver_destroy(&controller->receiver); } bool -controller_push_event(struct controller *controller, - const struct control_event *event) { +controller_push_msg(struct controller *controller, + const struct control_msg *msg) { mutex_lock(controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); - bool res = cbuf_push(&controller->queue, *event); + bool res = cbuf_push(&controller->queue, *msg); if (was_empty) { - cond_signal(controller->event_cond); + cond_signal(controller->msg_cond); } mutex_unlock(controller->mutex); return res; } static bool -process_event(struct controller *controller, - const struct control_event *event) { - unsigned char serialized_event[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int length = control_event_serialize(event, serialized_event); +process_msg(struct controller *controller, + const struct control_msg *msg) { + unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; } - int w = net_send_all(controller->control_socket, serialized_event, length); + int w = net_send_all(controller->control_socket, serialized_msg, length); return w == length; } @@ -76,22 +76,22 @@ run_controller(void *data) { for (;;) { mutex_lock(controller->mutex); while (!controller->stopped && cbuf_is_empty(&controller->queue)) { - cond_wait(controller->event_cond, controller->mutex); + cond_wait(controller->msg_cond, controller->mutex); } if (controller->stopped) { - // stop immediately, do not process further events + // stop immediately, do not process further msgs mutex_unlock(controller->mutex); break; } - struct control_event event; - bool non_empty = cbuf_take(&controller->queue, &event); + struct control_msg msg; + bool non_empty = cbuf_take(&controller->queue, &msg); SDL_assert(non_empty); mutex_unlock(controller->mutex); - bool ok = process_event(controller, &event); - control_event_destroy(&event); + bool ok = process_msg(controller, &msg); + control_msg_destroy(&msg); if (!ok) { - LOGD("Cannot write event to socket"); + LOGD("Cannot write msg to socket"); break; } } @@ -122,7 +122,7 @@ void controller_stop(struct controller *controller) { mutex_lock(controller->mutex); controller->stopped = true; - cond_signal(controller->event_cond); + cond_signal(controller->msg_cond); mutex_unlock(controller->mutex); } diff --git a/app/src/controller.h b/app/src/controller.h index a54859a7..ae13e39f 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,24 +1,24 @@ -#ifndef CONTROL_H -#define CONTROL_H +#ifndef CONTROLLER_H +#define CONTROLLER_H #include #include #include #include "cbuf.h" -#include "control_event.h" +#include "control_msg.h" #include "net.h" #include "receiver.h" -struct control_event_queue CBUF(struct control_event, 64); +struct control_msg_queue CBUF(struct control_msg, 64); struct controller { socket_t control_socket; SDL_Thread *thread; SDL_mutex *mutex; - SDL_cond *event_cond; + SDL_cond *msg_cond; bool stopped; - struct control_event_queue queue; + struct control_msg_queue queue; struct receiver receiver; }; @@ -38,7 +38,7 @@ void controller_join(struct controller *controller); bool -controller_push_event(struct controller *controller, - const struct control_event *event); +controller_push_msg(struct controller *controller, + const struct control_msg *msg); #endif diff --git a/app/src/convert.c b/app/src/convert.c index 504befe0..adf6d400 100644 --- a/app/src/convert.c +++ b/app/src/convert.c @@ -159,19 +159,19 @@ convert_mouse_buttons(uint32_t state) { bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_KEYCODE; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - if (!convert_keycode_action(from->type, &to->keycode_event.action)) { + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { return false; } uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) { + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { return false; } - to->keycode_event.metastate = convert_meta_state(mod); + to->inject_keycode.metastate = convert_meta_state(mod); return true; } @@ -179,17 +179,18 @@ input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_MOUSE; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; - if (!convert_mouse_action(from->type, &to->mouse_event.action)) { + if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { return false; } - to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); - to->mouse_event.position.screen_size = screen_size; - to->mouse_event.position.point.x = from->x; - to->mouse_event.position.point.y = from->y; + to->inject_mouse_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + to->inject_mouse_event.position.screen_size = screen_size; + to->inject_mouse_event.position.point.x = from->x; + to->inject_mouse_event.position.point.y = from->y; return true; } @@ -197,13 +198,13 @@ mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_MOUSE; - to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE; - to->mouse_event.buttons = convert_mouse_buttons(from->state); - to->mouse_event.position.screen_size = screen_size; - to->mouse_event.position.point.x = from->x; - to->mouse_event.position.point.y = from->y; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; + to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; + to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); + to->inject_mouse_event.position.screen_size = screen_size; + to->inject_mouse_event.position.point.x = from->x; + to->inject_mouse_event.position.point.y = from->y; return true; } @@ -211,17 +212,17 @@ mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, struct position position, - struct control_event *to) { - to->type = CONTROL_EVENT_TYPE_SCROLL; + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - to->scroll_event.position = position; + to->inject_scroll_event.position = position; int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; // SDL behavior seems inconsistent between horizontal and vertical scrolling // so reverse the horizontal // - to->scroll_event.hscroll = -mul * from->x; - to->scroll_event.vscroll = mul * from->y; + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; return true; } diff --git a/app/src/convert.h b/app/src/convert.h index 22cf1023..5989e163 100644 --- a/app/src/convert.h +++ b/app/src/convert.h @@ -4,7 +4,7 @@ #include #include -#include "control_event.h" +#include "control_msg.h" struct complete_mouse_motion_event { SDL_MouseMotionEvent *mouse_motion_event; @@ -18,24 +18,24 @@ struct complete_mouse_wheel_event { bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, - struct control_event *to); + struct control_msg *to); bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_event *to); + struct control_msg *to); // the video size may be different from the real device size, so we need the // size to which the absolute position apply, to scale it accordingly bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_event *to); + struct control_msg *to); // on Android, a scroll event requires the current mouse position bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, struct position position, - struct control_event *to); + struct control_msg *to); #endif diff --git a/app/src/device_event.h b/app/src/device_event.h deleted file mode 100644 index 8e0e8e7f..00000000 --- a/app/src/device_event.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef DEVICEEVENT_H -#define DEVICEEVENT_H - -#include -#include -#include - -#define DEVICE_EVENT_QUEUE_SIZE 64 -#define DEVICE_EVENT_TEXT_MAX_LENGTH 4093 -#define DEVICE_EVENT_SERIALIZED_MAX_SIZE (3 + DEVICE_EVENT_TEXT_MAX_LENGTH) - -enum device_event_type { - DEVICE_EVENT_TYPE_GET_CLIPBOARD, -}; - -struct device_event { - enum device_event_type type; - union { - struct { - char *text; // owned, to be freed by SDL_free() - } clipboard_event; - }; -}; - -// return the number of bytes consumed (0 for no event available, -1 on error) -ssize_t -device_event_deserialize(const unsigned char *buf, size_t len, - struct device_event *event); - -void -device_event_destroy(struct device_event *event); - -#endif diff --git a/app/src/device_event.c b/app/src/device_msg.c similarity index 61% rename from app/src/device_event.c rename to app/src/device_msg.c index 5bc70d99..a90d78dd 100644 --- a/app/src/device_event.c +++ b/app/src/device_msg.c @@ -1,4 +1,4 @@ -#include "device_event.h" +#include "device_msg.h" #include #include @@ -7,16 +7,16 @@ #include "log.h" ssize_t -device_event_deserialize(const unsigned char *buf, size_t len, - struct device_event *event) { +device_msg_deserialize(const unsigned char *buf, size_t len, + struct device_msg *msg) { if (len < 3) { // at least type + empty string length return 0; // not available } - event->type = buf[0]; - switch (event->type) { - case DEVICE_EVENT_TYPE_GET_CLIPBOARD: { + 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) { return 0; // not available @@ -31,18 +31,18 @@ device_event_deserialize(const unsigned char *buf, size_t len, } text[clipboard_len] = '\0'; - event->clipboard_event.text = text; + msg->clipboard.text = text; return 3 + clipboard_len; } default: - LOGW("Unsupported device event type: %d", (int) event->type); + LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover } } void -device_event_destroy(struct device_event *event) { - if (event->type == DEVICE_EVENT_TYPE_GET_CLIPBOARD) { - SDL_free(event->clipboard_event.text); +device_msg_destroy(struct device_msg *msg) { + if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { + SDL_free(msg->clipboard.text); } } diff --git a/app/src/device_msg.h b/app/src/device_msg.h new file mode 100644 index 00000000..fd4a7eb1 --- /dev/null +++ b/app/src/device_msg.h @@ -0,0 +1,32 @@ +#ifndef DEVICEMSG_H +#define DEVICEMSG_H + +#include +#include +#include + +#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 +#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) + +enum device_msg_type { + DEVICE_MSG_TYPE_CLIPBOARD, +}; + +struct device_msg { + enum device_msg_type type; + union { + struct { + char *text; // owned, to be freed by SDL_free() + } clipboard; + }; +}; + +// return the number of bytes consumed (0 for no msg available, -1 on error) +ssize_t +device_msg_deserialize(const unsigned char *buf, size_t len, + struct device_msg *msg); + +void +device_msg_destroy(struct device_msg *msg); + +#endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 23073cbc..a040913b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -39,23 +39,23 @@ static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) { // send DOWN event - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_KEYCODE; - control_event.keycode_event.keycode = keycode; - control_event.keycode_event.metastate = 0; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg.inject_keycode.keycode = keycode; + msg.inject_keycode.metastate = 0; if (actions & ACTION_DOWN) { - control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot send %s (DOWN)", name); + msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject %s (DOWN)'", name); return; } } if (actions & ACTION_UP) { - control_event.keycode_event.action = AKEY_EVENT_ACTION_UP; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot send %s (UP)", name); + msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'inject %s (UP)'", name); } } } @@ -98,41 +98,41 @@ action_menu(struct controller *controller, int actions) { // 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_BACK_OR_SCREEN_ON; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot turn screen on"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'turn screen on'"); } } static void expand_notification_panel(struct controller *controller) { - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot expand notification panel"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'expand notification panel'"); } } static void collapse_notification_panel(struct controller *controller) { - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot collapse notification panel"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'collapse notification panel'"); } } static void request_device_clipboard(struct controller *controller) { - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_GET_CLIPBOARD; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; - if (!controller_push_event(controller, &control_event)) { - LOGW("Cannot get device clipboard"); + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request device clipboard"); } } @@ -149,13 +149,13 @@ set_device_clipboard(struct controller *controller) { return; } - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_SET_CLIPBOARD; - control_event.set_clipboard_event.text = text; + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.text = text; - if (!controller_push_event(controller, &control_event)) { + if (!controller_push_msg(controller, &msg)) { SDL_free(text); - LOGW("Cannot send clipboard paste event"); + LOGW("Cannot request 'set device clipboard'"); } } @@ -185,12 +185,12 @@ clipboard_paste(struct controller *controller) { return; } - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_TEXT; - control_event.text_event.text = text; - if (!controller_push_event(controller, &control_event)) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = text; + if (!controller_push_msg(controller, &msg)) { SDL_free(text); - LOGW("Cannot send clipboard paste event"); + LOGW("Cannot request 'paste clipboard'"); } } @@ -203,16 +203,16 @@ input_manager_process_text_input(struct input_manager *input_manager, // letters and space are handled as raw key event return; } - struct control_event control_event; - control_event.type = CONTROL_EVENT_TYPE_TEXT; - control_event.text_event.text = SDL_strdup(event->text); - if (!control_event.text_event.text) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = SDL_strdup(event->text); + if (!msg.inject_text.text) { LOGW("Cannot strdup input text"); return; } - if (!controller_push_event(input_manager->controller, &control_event)) { - SDL_free(control_event.text_event.text); - LOGW("Cannot send text event"); + if (!controller_push_msg(input_manager->controller, &msg)) { + SDL_free(msg.inject_text.text); + LOGW("Cannot request 'inject text'"); } } @@ -344,10 +344,10 @@ input_manager_process_key(struct input_manager *input_manager, return; } - struct control_event control_event; - if (input_key_from_sdl_to_android(event, &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send control event"); + struct control_msg msg; + if (input_key_from_sdl_to_android(event, &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject keycode'"); } } } @@ -359,12 +359,12 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, // do not send motion events when no button is pressed return; } - struct control_event control_event; + struct control_msg msg; if (mouse_motion_from_sdl_to_android(event, input_manager->screen->frame_size, - &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send mouse motion event"); + &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse motion event'"); } } } @@ -391,9 +391,8 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { - bool outside = is_outside_device_screen(input_manager, - event->x, - event->y); + bool outside = + is_outside_device_screen(input_manager, event->x, event->y); if (outside) { screen_resize_to_fit(input_manager->screen); return; @@ -406,12 +405,12 @@ input_manager_process_mouse_button(struct input_manager *input_manager, return; } - struct control_event control_event; + struct control_msg msg; if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, - &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send mouse button event"); + &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse button event'"); } } } @@ -423,10 +422,10 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager, .screen_size = input_manager->screen->frame_size, .point = get_mouse_point(input_manager->screen), }; - struct control_event control_event; - if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) { - if (!controller_push_event(input_manager->controller, &control_event)) { - LOGW("Cannot send mouse wheel event"); + struct control_msg msg; + if (mouse_wheel_from_sdl_to_android(event, position, &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Cannot request 'inject mouse wheel event'"); } } } diff --git a/app/src/receiver.c b/app/src/receiver.c index 683b61fa..0f989489 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -4,8 +4,7 @@ #include #include "config.h" -#include "device_event.h" -#include "events.h" +#include "device_msg.h" #include "lock_util.h" #include "log.h" @@ -24,21 +23,20 @@ receiver_destroy(struct receiver *receiver) { } static void -process_event(struct receiver *receiver, struct device_event *event) { - switch (event->type) { - case DEVICE_EVENT_TYPE_GET_CLIPBOARD: - SDL_SetClipboardText(event->clipboard_event.text); +process_msg(struct receiver *receiver, struct device_msg *msg) { + switch (msg->type) { + case DEVICE_MSG_TYPE_CLIPBOARD: + SDL_SetClipboardText(msg->clipboard.text); break; } } static ssize_t -process_events(struct receiver *receiver, const unsigned char *buf, - size_t len) { +process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { - struct device_event event; - ssize_t r = device_event_deserialize(&buf[head], len - head, &event); + struct device_msg msg; + ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); if (r == -1) { return -1; } @@ -46,8 +44,8 @@ process_events(struct receiver *receiver, const unsigned char *buf, return head; } - process_event(receiver, &event); - device_event_destroy(&event); + process_msg(receiver, &msg); + device_msg_destroy(&msg); head += r; SDL_assert(head <= len); @@ -61,19 +59,19 @@ static int run_receiver(void *data) { struct receiver *receiver = data; - unsigned char buf[DEVICE_EVENT_SERIALIZED_MAX_SIZE]; + unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; size_t head = 0; for (;;) { - SDL_assert(head < DEVICE_EVENT_SERIALIZED_MAX_SIZE); + SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf, - DEVICE_EVENT_SERIALIZED_MAX_SIZE - head); + DEVICE_MSG_SERIALIZED_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); break; } - ssize_t consumed = process_events(receiver, buf, r); + ssize_t consumed = process_msgs(receiver, buf, r); if (consumed == -1) { // an error occurred break; diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c deleted file mode 100644 index d9cd0b76..00000000 --- a/app/tests/test_control_event_serialize.c +++ /dev/null @@ -1,228 +0,0 @@ -#include -#include - -#include "control_event.h" - -static void test_serialize_keycode_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_KEYCODE, - .keycode_event = { - .action = AKEY_EVENT_ACTION_UP, - .keycode = AKEYCODE_ENTER, - .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, - }, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 10); - - const unsigned char expected[] = { - 0x00, // CONTROL_EVENT_TYPE_KEYCODE - 0x01, // AKEY_EVENT_ACTION_UP - 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER - 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_text_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_TEXT, - .text_event = { - .text = "hello, world!", - }, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 16); - - const unsigned char expected[] = { - 0x01, // CONTROL_EVENT_TYPE_TEXT - 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(void) { - struct control_event event; - event.type = CONTROL_EVENT_TYPE_TEXT; - char text[CONTROL_EVENT_TEXT_MAX_LENGTH + 1]; - memset(text, 'a', sizeof(text)); - text[CONTROL_EVENT_TEXT_MAX_LENGTH] = '\0'; - event.text_event.text = text; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 3 + CONTROL_EVENT_TEXT_MAX_LENGTH); - - unsigned char expected[3 + CONTROL_EVENT_TEXT_MAX_LENGTH]; - expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE - expected[1] = 0x01; - expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_EVENT_TEXT_MAX_LENGTH); - - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_mouse_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_MOUSE, - .mouse_event = { - .action = AMOTION_EVENT_ACTION_DOWN, - .buttons = AMOTION_EVENT_BUTTON_PRIMARY, - .position = { - .point = { - .x = 260, - .y = 1026, - }, - .screen_size = { - .width = 1080, - .height = 1920, - }, - }, - }, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 18); - - const unsigned char expected[] = { - 0x02, // CONTROL_EVENT_TYPE_MOUSE - 0x00, // AKEY_EVENT_ACTION_DOWN - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 - 0x04, 0x38, 0x07, 0x80, // 1080 1920 - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_scroll_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_SCROLL, - .scroll_event = { - .position = { - .point = { - .x = 260, - .y = 1026, - }, - .screen_size = { - .width = 1080, - .height = 1920, - }, - }, - .hscroll = 1, - .vscroll = -1, - }, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 21); - - const unsigned char expected[] = { - 0x03, // CONTROL_EVENT_TYPE_SCROLL - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 - 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x00, 0x00, 0x00, 0x01, // 1 - 0xFF, 0xFF, 0xFF, 0xFF, // -1 - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_back_or_screen_on_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 1); - - const unsigned char expected[] = { - 0x04, // CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_expand_notification_panel_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 1); - - const unsigned char expected[] = { - 0x05, // CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_collapse_notification_panel_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 1); - - const unsigned char expected[] = { - 0x06, // CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_get_clipboard_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_GET_CLIPBOARD, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 1); - - const unsigned char expected[] = { - 0x07, // CONTROL_EVENT_TYPE_GET_CLIPBOARD - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -static void test_serialize_set_clipboard_event(void) { - struct control_event event = { - .type = CONTROL_EVENT_TYPE_SET_CLIPBOARD, - .text_event = { - .text = "hello, world!", - }, - }; - - unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; - int size = control_event_serialize(&event, buf); - assert(size == 16); - - const unsigned char expected[] = { - 0x08, // CONTROL_EVENT_TYPE_SET_CLIPBOARD - 0x00, 0x0d, // text length - 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - -int main(void) { - test_serialize_keycode_event(); - test_serialize_text_event(); - test_serialize_long_text_event(); - test_serialize_mouse_event(); - test_serialize_scroll_event(); - test_serialize_back_or_screen_on_event(); - test_serialize_expand_notification_panel_event(); - test_serialize_collapse_notification_panel_event(); - test_serialize_get_clipboard_event(); - test_serialize_set_clipboard_event(); - return 0; -} diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c new file mode 100644 index 00000000..20f72505 --- /dev/null +++ b/app/tests/test_control_msg_serialize.c @@ -0,0 +1,228 @@ +#include +#include + +#include "control_msg.h" + +static void test_serialize_inject_keycode(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, + .inject_keycode = { + .action = AKEY_EVENT_ACTION_UP, + .keycode = AKEYCODE_ENTER, + .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 10); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_KEYCODE, + 0x01, // AKEY_EVENT_ACTION_UP + 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER + 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_text(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TEXT, + .inject_text = { + .text = "hello, world!", + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 16); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_TEXT, + 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_inject_text_long(void) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', sizeof(text)); + text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; + msg.inject_text.text = text; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); + + unsigned char expected[3 + CONTROL_MSG_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_TEXT_MAX_LENGTH); + + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_mouse_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + .inject_mouse_event = { + .action = AMOTION_EVENT_ACTION_DOWN, + .buttons = AMOTION_EVENT_BUTTON_PRIMARY, + .position = { + .point = { + .x = 260, + .y = 1026, + }, + .screen_size = { + .width = 1080, + .height = 1920, + }, + }, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 18); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + 0x00, // AKEY_EVENT_ACTION_DOWN + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY + 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 + 0x04, 0x38, 0x07, 0x80, // 1080 1920 + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_inject_scroll_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + .inject_scroll_event = { + .position = { + .point = { + .x = 260, + .y = 1026, + }, + .screen_size = { + .width = 1080, + .height = 1920, + }, + }, + .hscroll = 1, + .vscroll = -1, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 21); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 + 0x04, 0x38, 0x07, 0x80, // 1080 1920 + 0x00, 0x00, 0x00, 0x01, // 1 + 0xFF, 0xFF, 0xFF, 0xFF, // -1 + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_back_or_screen_on(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_expand_notification_panel(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_collapse_notification_panel(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_get_clipboard(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_GET_CLIPBOARD, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_set_clipboard(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + .inject_text = { + .text = "hello, world!", + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 16); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_CLIPBOARD, + 0x00, 0x0d, // text length + 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +int main(void) { + test_serialize_inject_keycode(); + test_serialize_inject_text(); + test_serialize_inject_text_long(); + test_serialize_inject_mouse_event(); + test_serialize_inject_scroll_event(); + test_serialize_back_or_screen_on(); + test_serialize_expand_notification_panel(); + test_serialize_collapse_notification_panel(); + test_serialize_get_clipboard(); + test_serialize_set_clipboard(); + return 0; +} diff --git a/app/tests/test_device_event_deserialize.c b/app/tests/test_device_event_deserialize.c deleted file mode 100644 index 83e5e02e..00000000 --- a/app/tests/test_device_event_deserialize.c +++ /dev/null @@ -1,28 +0,0 @@ -#include -#include - -#include "device_event.h" - -#include -static void test_deserialize_clipboard_event(void) { - const unsigned char input[] = { - 0x00, // DEVICE_EVENT_TYPE_CLIPBOARD - 0x00, 0x03, // text length - 0x41, 0x42, 0x43, // "ABC" - }; - - struct device_event event; - ssize_t r = device_event_deserialize(input, sizeof(input), &event); - assert(r == 6); - - assert(event.type == DEVICE_EVENT_TYPE_GET_CLIPBOARD); - assert(event.clipboard_event.text); - assert(!strcmp("ABC", event.clipboard_event.text)); - - device_event_destroy(&event); -} - -int main(void) { - test_deserialize_clipboard_event(); - return 0; -} diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c new file mode 100644 index 00000000..e163ad72 --- /dev/null +++ b/app/tests/test_device_msg_deserialize.c @@ -0,0 +1,28 @@ +#include +#include + +#include "device_msg.h" + +#include +static void test_deserialize_clipboard(void) { + const unsigned char input[] = { + DEVICE_MSG_TYPE_CLIPBOARD, + 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(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); + assert(msg.clipboard.text); + assert(!strcmp("ABC", msg.clipboard.text)); + + device_msg_destroy(&msg); +} + +int main(void) { + test_deserialize_clipboard(); + return 0; +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java similarity index 61% rename from server/src/main/java/com/genymobile/scrcpy/ControlEvent.java rename to server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 51360560..093e293f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -3,12 +3,12 @@ package com.genymobile.scrcpy; /** * Union of all supported event types, identified by their {@code type}. */ -public final class ControlEvent { +public final class ControlMessage { - public static final int TYPE_KEYCODE = 0; - public static final int TYPE_TEXT = 1; - public static final int TYPE_MOUSE = 2; - public static final int TYPE_SCROLL = 3; + public static final int TYPE_INJECT_KEYCODE = 0; + public static final int TYPE_INJECT_TEXT = 1; + public static final int TYPE_INJECT_MOUSE_EVENT = 2; + public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; @@ -25,52 +25,52 @@ public final class ControlEvent { private int hScroll; private int vScroll; - private ControlEvent() { + private ControlMessage() { } - public static ControlEvent createKeycodeControlEvent(int action, int keycode, int metaState) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_KEYCODE; + public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_KEYCODE; event.action = action; event.keycode = keycode; event.metaState = metaState; return event; } - public static ControlEvent createTextControlEvent(String text) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_TEXT; + public static ControlMessage createInjectText(String text) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_TEXT; event.text = text; return event; } - public static ControlEvent createMotionControlEvent(int action, int buttons, Position position) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_MOUSE; + public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_MOUSE_EVENT; event.action = action; event.buttons = buttons; event.position = position; return event; } - public static ControlEvent createScrollControlEvent(Position position, int hScroll, int vScroll) { - ControlEvent event = new ControlEvent(); - event.type = TYPE_SCROLL; + public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_INJECT_SCROLL_EVENT; event.position = position; event.hScroll = hScroll; event.vScroll = vScroll; return event; } - public static ControlEvent createSetClipboardControlEvent(String text) { - ControlEvent event = new ControlEvent(); + public static ControlMessage createSetClipboard(String text) { + ControlMessage event = new ControlMessage(); event.type = TYPE_SET_CLIPBOARD; event.text = text; return event; } - public static ControlEvent createSimpleControlEvent(int type) { - ControlEvent event = new ControlEvent(); + public static ControlMessage createEmpty(int type) { + ControlMessage event = new ControlMessage(); event.type = type; return event; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java similarity index 61% rename from server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java rename to server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index ec807232..965cd29a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -6,11 +6,11 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -public class ControlEventReader { +public class ControlMessageReader { - private static final int KEYCODE_PAYLOAD_LENGTH = 9; - private static final int MOUSE_PAYLOAD_LENGTH = 17; - private static final int SCROLL_PAYLOAD_LENGTH = 20; + private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; + private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; + private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; @@ -20,7 +20,7 @@ public class ControlEventReader { private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; - public ControlEventReader() { + public ControlMessageReader() { // invariant: the buffer is always in "get" mode buffer.limit(0); } @@ -37,63 +37,63 @@ public class ControlEventReader { int head = buffer.position(); int r = input.read(rawBuffer, head, rawBuffer.length - head); if (r == -1) { - throw new EOFException("Event controller socket closed"); + throw new EOFException("Controller socket closed"); } buffer.position(head + r); buffer.flip(); } - public ControlEvent next() { + public ControlMessage next() { if (!buffer.hasRemaining()) { return null; } int savedPosition = buffer.position(); int type = buffer.get(); - ControlEvent controlEvent; + ControlMessage msg; switch (type) { - case ControlEvent.TYPE_KEYCODE: - controlEvent = parseKeycodeControlEvent(); + case ControlMessage.TYPE_INJECT_KEYCODE: + msg = parseInjectKeycode(); break; - case ControlEvent.TYPE_TEXT: - controlEvent = parseTextControlEvent(); + case ControlMessage.TYPE_INJECT_TEXT: + msg = parseInjectText(); break; - case ControlEvent.TYPE_MOUSE: - controlEvent = parseMouseControlEvent(); + case ControlMessage.TYPE_INJECT_MOUSE_EVENT: + msg = parseInjectMouseEvent(); break; - case ControlEvent.TYPE_SCROLL: - controlEvent = parseScrollControlEvent(); + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + msg = parseInjectScrollEvent(); break; - case ControlEvent.TYPE_SET_CLIPBOARD: - controlEvent = parseSetClipboardEvent(); + case ControlMessage.TYPE_SET_CLIPBOARD: + msg = parseSetClipboard(); break; - case ControlEvent.TYPE_BACK_OR_SCREEN_ON: - case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: - case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: - case ControlEvent.TYPE_GET_CLIPBOARD: - controlEvent = ControlEvent.createSimpleControlEvent(type); + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = ControlMessage.createEmpty(type); break; default: Ln.w("Unknown event type: " + type); - controlEvent = null; + msg = null; break; } - if (controlEvent == null) { + if (msg == null) { // failure, reset savedPosition buffer.position(savedPosition); } - return controlEvent; + return msg; } - private ControlEvent parseKeycodeControlEvent() { - if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) { + private ControlMessage parseInjectKeycode() { + if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { return null; } int action = toUnsigned(buffer.get()); int keycode = buffer.getInt(); int metaState = buffer.getInt(); - return ControlEvent.createKeycodeControlEvent(action, keycode, metaState); + return ControlMessage.createInjectKeycode(action, keycode, metaState); } private String parseString() { @@ -108,40 +108,40 @@ public class ControlEventReader { return new String(textBuffer, 0, len, StandardCharsets.UTF_8); } - private ControlEvent parseTextControlEvent() { + private ControlMessage parseInjectText() { String text = parseString(); if (text == null) { return null; } - return ControlEvent.createTextControlEvent(text); + return ControlMessage.createInjectText(text); } - private ControlEvent parseMouseControlEvent() { - if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) { + private ControlMessage parseInjectMouseEvent() { + if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { return null; } int action = toUnsigned(buffer.get()); int buttons = buffer.getInt(); Position position = readPosition(buffer); - return ControlEvent.createMotionControlEvent(action, buttons, position); + return ControlMessage.createInjectMouseEvent(action, buttons, position); } - private ControlEvent parseScrollControlEvent() { - if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) { + private ControlMessage parseInjectScrollEvent() { + if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { return null; } Position position = readPosition(buffer); int hScroll = buffer.getInt(); int vScroll = buffer.getInt(); - return ControlEvent.createScrollControlEvent(position, hScroll, vScroll); + return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); } - private ControlEvent parseSetClipboardEvent() { + private ControlMessage parseSetClipboard() { String text = parseString(); if (text == null) { return null; } - return ControlEvent.createSetClipboardControlEvent(text); + return ControlMessage.createSetClipboard(text); } private static Position readPosition(ByteBuffer buffer) { diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java similarity index 81% rename from server/src/main/java/com/genymobile/scrcpy/EventController.java rename to server/src/main/java/com/genymobile/scrcpy/Controller.java index e8cd2d68..c9215f50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -11,11 +11,11 @@ import android.view.MotionEvent; import java.io.IOException; -public class EventController { +public class Controller { private final Device device; private final DesktopConnection connection; - private final EventSender sender; + private final DeviceMessageSender sender; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -23,11 +23,11 @@ public class EventController { private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; - public EventController(Device device, DesktopConnection connection) { + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; initPointer(); - sender = new EventSender(connection); + sender = new DeviceMessageSender(connection); } private void initPointer() { @@ -62,40 +62,40 @@ public class EventController { } } - public EventSender getSender() { + public DeviceMessageSender getSender() { return sender; } private void handleEvent() throws IOException { - ControlEvent controlEvent = connection.receiveControlEvent(); - switch (controlEvent.getType()) { - case ControlEvent.TYPE_KEYCODE: - injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState()); + ControlMessage msg = connection.receiveControlMessage(); + switch (msg.getType()) { + case ControlMessage.TYPE_INJECT_KEYCODE: + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); break; - case ControlEvent.TYPE_TEXT: - injectText(controlEvent.getText()); + case ControlMessage.TYPE_INJECT_TEXT: + injectText(msg.getText()); break; - case ControlEvent.TYPE_MOUSE: - injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition()); + case ControlMessage.TYPE_INJECT_MOUSE_EVENT: + injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); break; - case ControlEvent.TYPE_SCROLL: - injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll()); + case ControlMessage.TYPE_INJECT_SCROLL_EVENT: + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); break; - case ControlEvent.TYPE_BACK_OR_SCREEN_ON: + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: pressBackOrTurnScreenOn(); break; - case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: + case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: device.expandNotificationPanel(); break; - case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: device.collapsePanels(); break; - case ControlEvent.TYPE_GET_CLIPBOARD: + case ControlMessage.TYPE_GET_CLIPBOARD: String clipboardText = device.getClipboardText(); sender.pushClipboardText(clipboardText); break; - case ControlEvent.TYPE_SET_CLIPBOARD: - device.setClipboardText(controlEvent.getText()); + case ControlMessage.TYPE_SET_CLIPBOARD: + device.setClipboardText(msg.getText()); break; default: // do nothing diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index dcc9863b..a26c74cd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -24,8 +24,8 @@ public final class DesktopConnection implements Closeable { private final InputStream controlInputStream; private final OutputStream controlOutputStream; - private final ControlEventReader reader = new ControlEventReader(); - private final DeviceEventWriter writer = new DeviceEventWriter(); + private final ControlMessageReader reader = new ControlMessageReader(); + private final DeviceMessageWriter writer = new DeviceMessageWriter(); private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { this.videoSocket = videoSocket; @@ -104,16 +104,16 @@ public final class DesktopConnection implements Closeable { return videoFd; } - public ControlEvent receiveControlEvent() throws IOException { - ControlEvent event = reader.next(); - while (event == null) { + public ControlMessage receiveControlMessage() throws IOException { + ControlMessage msg = reader.next(); + while (msg == null) { reader.readFrom(controlInputStream); - event = reader.next(); + msg = reader.next(); } - return event; + return msg; } - public void sendDeviceEvent(DeviceEvent event) throws IOException { - writer.writeTo(event, controlOutputStream); + public void sendDeviceMessage(DeviceMessage msg) throws IOException { + writer.writeTo(msg, controlOutputStream); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java b/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java deleted file mode 100644 index 97bcbfc6..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.genymobile.scrcpy; - -public final class DeviceEvent { - - public static final int TYPE_GET_CLIPBOARD = 0; - - private int type; - private String text; - - private DeviceEvent() { - } - - public static DeviceEvent createGetClipboardEvent(String text) { - DeviceEvent event = new DeviceEvent(); - event.type = TYPE_GET_CLIPBOARD; - event.text = text; - return event; - } - - public int getType() { - return type; - } - - public String getText() { - return text; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java new file mode 100644 index 00000000..c6eebd38 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -0,0 +1,27 @@ +package com.genymobile.scrcpy; + +public final class DeviceMessage { + + public static final int TYPE_CLIPBOARD = 0; + + private int type; + private String text; + + private DeviceMessage() { + } + + public static DeviceMessage createClipboard(String text) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_CLIPBOARD; + event.text = text; + return event; + } + + public int getType() { + return type; + } + + public String getText() { + return text; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/EventSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java similarity index 74% rename from server/src/main/java/com/genymobile/scrcpy/EventSender.java rename to server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 9f50b16a..bbf4dd2e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -2,13 +2,13 @@ package com.genymobile.scrcpy; import java.io.IOException; -public final class EventSender { +public final class DeviceMessageSender { private final DesktopConnection connection; private String clipboardText; - public EventSender(DesktopConnection connection) { + public DeviceMessageSender(DesktopConnection connection) { this.connection = connection; } @@ -27,8 +27,8 @@ public final class EventSender { text = clipboardText; clipboardText = null; } - DeviceEvent event = DeviceEvent.createGetClipboardEvent(text); - connection.sendDeviceEvent(event); + DeviceMessage event = DeviceMessage.createClipboard(text); + connection.sendDeviceMessage(event); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java similarity index 72% rename from server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java rename to server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index e183a221..e2a3a1a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -5,7 +5,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -public class DeviceEventWriter { +public class DeviceMessageWriter { public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; @@ -14,12 +14,12 @@ public class DeviceEventWriter { private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); @SuppressWarnings("checkstyle:MagicNumber") - public void writeTo(DeviceEvent event, OutputStream output) throws IOException { + public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); - buffer.put((byte) DeviceEvent.TYPE_GET_CLIPBOARD); - switch (event.getType()) { - case DeviceEvent.TYPE_GET_CLIPBOARD: - String text = event.getText(); + buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); + switch (msg.getType()) { + case DeviceMessage.TYPE_CLIPBOARD: + String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); buffer.putShort((short) len); @@ -27,7 +27,7 @@ public class DeviceEventWriter { output.write(rawBuffer, 0, buffer.position()); break; default: - Ln.w("Unknown device event: " + event.getType()); + Ln.w("Unknown device message: " + msg.getType()); break; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 25cb15e6..76028fbe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -19,11 +19,11 @@ public final class Server { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); - EventController controller = new EventController(device, connection); + Controller controller = new Controller(device, connection); // asynchronous - startEventController(controller); - startEventSender(controller.getSender()); + startController(controller); + startDeviceMessageSender(controller.getSender()); try { // synchronous @@ -35,7 +35,7 @@ public final class Server { } } - private static void startEventController(final EventController controller) { + private static void startController(final Controller controller) { new Thread(new Runnable() { @Override public void run() { @@ -43,13 +43,13 @@ public final class Server { controller.control(); } catch (IOException e) { // this is expected on close - Ln.d("Event controller stopped"); + Ln.d("Controller stopped"); } } }).start(); } - private static void startEventSender(final EventSender sender) { + private static void startDeviceMessageSender(final DeviceMessageSender sender) { new Thread(new Runnable() { @Override public void run() { @@ -57,7 +57,7 @@ public final class Server { sender.loop(); } catch (IOException | InterruptedException e) { // this is expected on close - Ln.d("Event sender stopped"); + Ln.d("Device message sender stopped"); } } }).start(); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java similarity index 69% rename from server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java rename to server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 692b5d23..788ee12e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -14,24 +14,24 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -public class ControlEventReaderTest { +public class ControlMessageReaderTest { @Test public void testParseKeycodeEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); @@ -39,59 +39,59 @@ public class ControlEventReaderTest { @Test public void testParseTextEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_TEXT); + dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeShort(text.length); dos.write(text); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals("testé", event.getText()); } @Test public void testParseLongTextEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_TEXT); - byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH]; + dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); + byte[] text = new byte[ControlMessageReader.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(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); } @Test public void testParseMouseEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); @@ -100,11 +100,11 @@ public class ControlEventReaderTest { @Test @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_SCROLL); + dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); dos.writeInt(260); dos.writeInt(1026); dos.writeShort(1080); @@ -115,9 +115,9 @@ public class ControlEventReaderTest { byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_SCROLL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); Assert.assertEquals(260, event.getPosition().getPoint().getX()); Assert.assertEquals(1026, event.getPosition().getPoint().getY()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); @@ -128,75 +128,75 @@ public class ControlEventReaderTest { @Test public void testParseBackOrScreenOnEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_BACK_OR_SCREEN_ON); + dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_BACK_OR_SCREEN_ON, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); } @Test public void testParseExpandNotificationPanelEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); } @Test public void testParseCollapseNotificationPanelEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); } @Test public void testParseGetClipboardEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_GET_CLIPBOARD); + dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_GET_CLIPBOARD, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); } @Test public void testParseSetClipboardEvent() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_SET_CLIPBOARD); + dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeShort(text.length); dos.write(text); @@ -204,25 +204,25 @@ public class ControlEventReaderTest { byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); + ControlMessage event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); } @Test public void testMultiEvents() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.META_CTRL_ON); - dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(KeyEvent.META_CTRL_ON); @@ -230,14 +230,14 @@ public class ControlEventReaderTest { byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + ControlMessage event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); @@ -245,24 +245,24 @@ public class ControlEventReaderTest { @Test public void testPartialEvents() throws IOException { - ControlEventReader reader = new ControlEventReader(); + ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.META_CTRL_ON); - dos.writeByte(ControlEvent.TYPE_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); - ControlEvent event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + ControlMessage event = reader.next(); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); @@ -278,7 +278,7 @@ public class ControlEventReaderTest { // the event is now complete event = reader.next(); - Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); From ad55a9addc4bd045b8af6bb541e58abc2b32460d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 15:31:38 +0200 Subject: [PATCH 49/90] Prefix server logs Sometimes, it is not obvious whether a log is generated by the server or by the client. Prefix server logs for clarity. --- server/src/main/java/com/genymobile/scrcpy/Ln.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index cd466b3e..bb741225 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -9,6 +9,7 @@ import android.util.Log; public final class Ln { private static final String TAG = "scrcpy"; + private static final String PREFIX = "[server] "; enum Level { DEBUG, @@ -30,28 +31,28 @@ public final class Ln { public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); - System.out.println("DEBUG: " + message); + System.out.println(PREFIX + "DEBUG: " + message); } } public static void i(String message) { if (isEnabled(Level.INFO)) { Log.i(TAG, message); - System.out.println("INFO: " + message); + System.out.println(PREFIX + "INFO: " + message); } } public static void w(String message) { if (isEnabled(Level.WARN)) { Log.w(TAG, message); - System.out.println("WARN: " + message); + System.out.println(PREFIX + "WARN: " + message); } } public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - System.out.println("ERROR: " + message); + System.out.println(PREFIX + "ERROR: " + message); if (throwable != null) { throwable.printStackTrace(); } From 9712cb8123502152ec6932a709c7d8de7ea02eb5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 15:35:53 +0200 Subject: [PATCH 50/90] Do not minimize on focus loss The default behavior seems annoying. Fixes --- app/src/scrcpy.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6cc0a3ec..0264561b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -76,6 +76,11 @@ sdl_init_and_configure(bool display) { } #endif + // Do not minimize on focus loss + if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { + LOGW("Could not disable minimize on focus loss"); + } + // Do not disable the screensaver when scrcpy is running SDL_EnableScreenSaver(); From 6537c2ef011cf730c4f26b047131041e065094ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 15:43:25 +0200 Subject: [PATCH 51/90] Add clipboard logs Synchronizing local and device clipboards in invisible. Add INFO logs on success. --- app/src/receiver.c | 1 + server/src/main/java/com/genymobile/scrcpy/Device.java | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/receiver.c b/app/src/receiver.c index 0f989489..1c80bb00 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -26,6 +26,7 @@ static void process_msg(struct receiver *receiver, struct device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: + LOGI("Device clipboard copied"); SDL_SetClipboardText(msg->clipboard.text); break; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index f791266b..f6219a93 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -149,6 +149,7 @@ public final class Device { public void setClipboardText(String text) { serviceManager.getClipboardManager().setText(text); + Ln.i("Device clipboard set"); } static Rect flipRect(Rect crop) { From fcf225049dc783677244f08700ed4c53e8dc5382 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Fri, 31 May 2019 22:08:30 +0800 Subject: [PATCH 52/90] Use consistent variable names Use the same variable name in functions declaration and definition. Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/command.h | 2 +- app/src/device.h | 2 +- app/src/recorder.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/command.h b/app/src/command.h index 90eb7cb2..3453ca10 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -74,6 +74,6 @@ adb_install(const char *serial, const char *local); // convenience function to wait for a successful process execution // automatically log process errors with the provided process name bool -process_check_success(process_t process, const char *name); +process_check_success(process_t proc, const char *name); #endif diff --git a/app/src/device.h b/app/src/device.h index 09934046..f3449e5e 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -11,6 +11,6 @@ // name must be at least DEVICE_NAME_FIELD_LENGTH bytes bool -device_read_info(socket_t device_socket, char *name, struct size *frame_size); +device_read_info(socket_t device_socket, char *device_name, struct size *size); #endif diff --git a/app/src/recorder.h b/app/src/recorder.h index 26c4a3c3..8a8e3310 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -20,7 +20,7 @@ struct recorder { }; bool -recorder_init(struct recorder *recoder, const char *filename, +recorder_init(struct recorder *recorder, const char *filename, enum recorder_format format, struct size declared_frame_size); void From a56045dd8047f9b147e63be67c41a4423023437f Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Fri, 31 May 2019 22:15:31 +0800 Subject: [PATCH 53/90] Prevent socket leak on error Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/net.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/net.c b/app/src/net.c index b5b227c2..1b487f7f 100644 --- a/app/src/net.c +++ b/app/src/net.c @@ -33,6 +33,7 @@ net_connect(uint32_t addr, uint16_t port) { if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { perror("connect"); + close(sock); return INVALID_SOCKET; } @@ -60,11 +61,13 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { perror("bind"); + close(sock); return INVALID_SOCKET; } if (listen(sock, backlog) == SOCKET_ERROR) { perror("listen"); + close(sock); return INVALID_SOCKET; } From 3ee9560ece4b4d17031e23b3ac8a0901f86c5694 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 22:33:39 +0200 Subject: [PATCH 54/90] Fix comment style For consistency. --- app/src/command.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/command.c b/app/src/command.c index 717455dd..4cb2e408 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -69,7 +69,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { "path in the ADB environment variable)"); break; case PROCESS_SUCCESS: - /* do nothing */ + // do nothing break; } } From 12a3bb25d39037931f577dc0165b3e517aad92df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Mar 2019 20:23:30 +0100 Subject: [PATCH 55/90] Implement device screen off while mirroring Add two shortcuts: - Ctrl+o to turn the device screen off while mirroring - Ctrl+Shift+o to turn it back on On power on (either via the POWER key or BACK while screen is off), both the device screen and the mirror are turned on. --- README.md | 4 +++- app/src/control_msg.c | 3 +++ app/src/control_msg.h | 10 ++++++++++ app/src/input_manager.c | 20 +++++++++++++++++++ app/src/main.c | 8 +++++++- app/tests/test_control_msg_serialize.c | 20 +++++++++++++++++++ .../com/genymobile/scrcpy/ControlMessage.java | 15 +++++++++++++- .../scrcpy/ControlMessageReader.java | 12 +++++++++++ .../com/genymobile/scrcpy/Controller.java | 3 +++ .../java/com/genymobile/scrcpy/Device.java | 14 +++++++++++++ .../scrcpy/wrappers/SurfaceControl.java | 20 +++++++++++++++++++ .../scrcpy/ControlMessageReaderTest.java | 18 +++++++++++++++++ 12 files changed, 144 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef1424ba..0ee6d79b 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,9 @@ you are interested, see [issue 14]. | click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) | | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | | click on `POWER` | `Ctrl`+`p` | - | turn screen on | _Right-click²_ | + | power on | _Right-click²_ | + | turn device screen off (keep mirroring)| `Ctrl`+`o` | + | turn device screen on | `Ctrl`+`Shift`+`o` | | expand notification panel | `Ctrl`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` | | copy device clipboard to computer | `Ctrl`+`c` | diff --git a/app/src/control_msg.c b/app/src/control_msg.c index ca1028b3..9c3d9849 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -55,6 +55,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { &buf[1]); return 1 + len; } + case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + buf[1] = msg->set_screen_power_mode.mode; + return 2; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index abc11f32..e7fdfc4c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -24,6 +24,13 @@ enum control_msg_type { CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, +}; + +enum screen_power_mode { + // see + SCREEN_POWER_MODE_OFF = 0, + SCREEN_POWER_MODE_NORMAL = 2, }; struct control_msg { @@ -50,6 +57,9 @@ struct control_msg { struct { char *text; // owned, to be freed by SDL_free() } set_clipboard; + struct { + enum screen_power_mode mode; + } set_screen_power_mode; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index a040913b..68648b11 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -159,6 +159,18 @@ set_device_clipboard(struct controller *controller) { } } +static void +set_screen_power_mode(struct controller *controller, + enum screen_power_mode mode) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = mode; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); + } +} + static void switch_fps_counter_state(struct video_buffer *vb) { mutex_lock(vb->mutex); @@ -263,6 +275,14 @@ input_manager_process_key(struct input_manager *input_manager, action_power(input_manager->controller, action); } return; + case SDLK_o: + if (control && ctrl && !meta && event->type == SDL_KEYDOWN) { + enum screen_power_mode mode = shift + ? SCREEN_POWER_MODE_NORMAL + : SCREEN_POWER_MODE_OFF; + set_screen_power_mode(input_manager->controller, mode); + } + return; case SDLK_DOWN: #ifdef __APPLE__ if (control && !ctrl && meta && !shift) { diff --git a/app/src/main.c b/app/src/main.c index be611025..d8568884 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -129,7 +129,13 @@ static void usage(const char *arg0) { " click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " turn screen on\n" + " power on\n" + "\n" + " Ctrl+o\n" + " turn device screen off (keep mirroring)\n" + "\n" + " Ctrl+Shift+o\n" + " turn device screen on\n" "\n" " Ctrl+n\n" " expand notification panel\n" diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 20f72505..c0c501f2 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -213,6 +213,25 @@ static void test_serialize_set_clipboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_set_screen_power_mode(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + .set_screen_power_mode = { + .mode = SCREEN_POWER_MODE_NORMAL, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 2); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SCREEN_POWER_MODE_NORMAL + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); @@ -224,5 +243,6 @@ int main(void) { test_serialize_collapse_notification_panel(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); + test_serialize_set_screen_power_mode(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 093e293f..3f2b8960 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.SurfaceControl; + /** * Union of all supported event types, identified by their {@code type}. */ @@ -14,11 +16,12 @@ public final class ControlMessage { public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_SET_CLIPBOARD = 8; + public static final int TYPE_SET_SCREEN_POWER_MODE = 9; private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* private Position position; @@ -69,6 +72,16 @@ public final class ControlMessage { return event; } + /** + * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants + */ + public static ControlMessage createSetScreenPowerMode(int mode) { + ControlMessage event = new ControlMessage(); + event.type = TYPE_SET_SCREEN_POWER_MODE; + event.action = mode; + return event; + } + public static ControlMessage createEmpty(int type) { ControlMessage event = new ControlMessage(); event.type = type; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 965cd29a..8ced049d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -11,6 +11,7 @@ public class ControlMessageReader { private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; public static final int TEXT_MAX_LENGTH = 300; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; @@ -67,6 +68,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + msg = parseSetScreenPowerMode(); + break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: @@ -144,6 +148,14 @@ public class ControlMessageReader { return ControlMessage.createSetClipboard(text); } + private ControlMessage parseSetScreenPowerMode() { + if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { + return null; + } + int mode = buffer.get(); + return ControlMessage.createSetScreenPowerMode(mode); + } + private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index c9215f50..dead6d3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -97,6 +97,9 @@ public class Controller { case ControlMessage.TYPE_SET_CLIPBOARD: device.setClipboardText(msg.getText()); break; + case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + device.setScreenPowerMode(msg.getAction()); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index f6219a93..538135d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,15 +1,20 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.os.Build; +import android.os.IBinder; import android.os.RemoteException; import android.view.IRotationWatcher; import android.view.InputEvent; public final class Device { + public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; + public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + public interface RotationListener { void onRotationChanged(int rotation); } @@ -152,6 +157,15 @@ public final class Device { Ln.i("Device clipboard set"); } + /** + * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + */ + public void setScreenPowerMode(int mode) { + IBinder d = SurfaceControl.getBuiltInDisplay(0); + SurfaceControl.setDisplayPowerMode(d, mode); + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 85733867..bed21b3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -10,6 +10,10 @@ public final class SurfaceControl { private static final Class CLASS; + // see + public static final int POWER_MODE_OFF = 0; + public static final int POWER_MODE_NORMAL = 2; + static { try { CLASS = Class.forName("android.view.SurfaceControl"); @@ -71,6 +75,22 @@ public final class SurfaceControl { } } + public static IBinder getBuiltInDisplay(int builtInDisplayId) { + try { + return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public static void setDisplayPowerMode(IBinder displayToken, int mode) { + try { + CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); + } catch (Exception e) { + throw new AssertionError(e); + } + } + public static void destroyDisplay(IBinder displayToken) { try { CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 788ee12e..df1db1a6 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -210,6 +210,24 @@ public class ControlMessageReaderTest { Assert.assertEquals("testé", event.getText()); } + @Test + public void testParseSetScreenPowerMode() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); + dos.writeByte(Device.POWER_MODE_NORMAL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); + Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From 0792998cc24de067bb2d4a055838fb5047ad6197 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 23:31:11 +0200 Subject: [PATCH 56/90] Remove unused import Introduced by the previous commit. --- server/src/main/java/com/genymobile/scrcpy/ControlMessage.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 3f2b8960..0de4bc3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.SurfaceControl; - /** * Union of all supported event types, identified by their {@code type}. */ From 296047d82a4d6e8d87a955b9643f7538e79200ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 23:32:53 +0200 Subject: [PATCH 57/90] Use net_close() to close sockets So that it also works on Windows. --- app/src/net.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/net.c b/app/src/net.c index 1b487f7f..a0bc38f2 100644 --- a/app/src/net.c +++ b/app/src/net.c @@ -33,7 +33,7 @@ net_connect(uint32_t addr, uint16_t port) { if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { perror("connect"); - close(sock); + net_close(sock); return INVALID_SOCKET; } @@ -61,13 +61,13 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { perror("bind"); - close(sock); + net_close(sock); return INVALID_SOCKET; } if (listen(sock, backlog) == SOCKET_ERROR) { perror("listen"); - close(sock); + net_close(sock); return INVALID_SOCKET; } From 41225c3e41324e8feb0a88d217f47d29a8dc9520 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 May 2019 23:25:41 +0200 Subject: [PATCH 58/90] Improve key processing readability The condition "event->type == SDL_KEYDOWN" and the variable input_manager->controller are used many times. Replace them by local variables to reduce verbosity. --- app/src/input_manager.c | 59 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 68648b11..10a84e17 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -232,6 +232,9 @@ void input_manager_process_key(struct input_manager *input_manager, const SDL_KeyboardEvent *event, bool control) { + // control: indicates the state of the command-line option --no-control + // ctrl: the Ctrl key + 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); @@ -242,45 +245,48 @@ input_manager_process_key(struct input_manager *input_manager, return; } + struct controller *controller = input_manager->controller; + // capture all Ctrl events if (ctrl | meta) { SDL_Keycode keycode = event->keysym.sym; - int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP; + bool down = event->type == SDL_KEYDOWN; + 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: if (control && ctrl && !meta && !shift && !repeat) { - action_home(input_manager->controller, action); + action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: if (control && ctrl && !meta && !shift && !repeat) { - action_back(input_manager->controller, action); + action_back(controller, action); } return; case SDLK_s: if (control && ctrl && !meta && !shift && !repeat) { - action_app_switch(input_manager->controller, action); + action_app_switch(controller, action); } return; case SDLK_m: if (control && ctrl && !meta && !shift && !repeat) { - action_menu(input_manager->controller, action); + action_menu(controller, action); } return; case SDLK_p: if (control && ctrl && !meta && !shift && !repeat) { - action_power(input_manager->controller, action); + action_power(controller, action); } return; case SDLK_o: - if (control && ctrl && !meta && event->type == SDL_KEYDOWN) { + if (control && ctrl && !meta && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; - set_screen_power_mode(input_manager->controller, mode); + set_screen_power_mode(controller, mode); } return; case SDLK_DOWN: @@ -290,7 +296,7 @@ input_manager_process_key(struct input_manager *input_manager, if (control && ctrl && !meta && !shift) { #endif // forward repeated events - action_volume_down(input_manager->controller, action); + action_volume_down(controller, action); } return; case SDLK_UP: @@ -300,58 +306,51 @@ input_manager_process_key(struct input_manager *input_manager, if (control && ctrl && !meta && !shift) { #endif // forward repeated events - action_volume_up(input_manager->controller, action); + action_volume_up(controller, action); } return; case SDLK_c: - if (control && ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { - request_device_clipboard(input_manager->controller); + if (control && ctrl && !meta && !shift && !repeat && down) { + request_device_clipboard(controller); } return; case SDLK_v: - if (control && ctrl && !meta && !repeat - && event->type == SDL_KEYDOWN) { + if (control && ctrl && !meta && !repeat && down) { if (shift) { // store the text in the device clipboard - set_device_clipboard(input_manager->controller); + set_device_clipboard(controller); } else { // inject the text as input events - clipboard_paste(input_manager->controller); + clipboard_paste(controller); } } return; case SDLK_f: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { screen_switch_fullscreen(input_manager->screen); } return; case SDLK_x: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { screen_resize_to_fit(input_manager->screen); } return; case SDLK_g: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { screen_resize_to_pixel_perfect(input_manager->screen); } return; case SDLK_i: - if (ctrl && !meta && !shift && !repeat - && event->type == SDL_KEYDOWN) { + if (ctrl && !meta && !shift && !repeat && down) { switch_fps_counter_state(input_manager->video_buffer); } return; case SDLK_n: - if (control && ctrl && !meta - && !repeat && event->type == SDL_KEYDOWN) { + if (control && ctrl && !meta && !repeat && down) { if (shift) { - collapse_notification_panel(input_manager->controller); + collapse_notification_panel(controller); } else { - expand_notification_panel(input_manager->controller); + expand_notification_panel(controller); } } return; @@ -366,7 +365,7 @@ input_manager_process_key(struct input_manager *input_manager, struct control_msg msg; if (input_key_from_sdl_to_android(event, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(controller, &msg)) { LOGW("Cannot request 'inject keycode'"); } } From e572d81fa25b930e8aa68da1160dd034a532c279 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Jun 2019 15:46:34 +0200 Subject: [PATCH 59/90] Rename function to "power on" This will reduce confusion between "power on" when the device is off and "turn device screen off" while mirroring. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dead6d3c..a43ee5ba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -54,8 +54,8 @@ public class Controller { } public void control() throws IOException { - // on start, turn screen on - turnScreenOn(); + // on start, power on the device + powerOn(); while (true) { handleEvent(); @@ -182,7 +182,7 @@ public class Controller { return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } - private boolean turnScreenOn() { + private boolean powerOn() { return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER); } From 8c8649cfcd710859ce18eab557ed2af8cedb9a42 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 3 Jun 2019 11:44:39 +0200 Subject: [PATCH 60/90] Remove "turn device screen on" feature Only keep "turn device screen off" and POWER button. After we turn the device screen off (with Ctrl+o), turning it back on does not always work, and leaves the device in a weird state, where even the power button may not be sufficient: This is not an acceptable behavior, so disable the shortcut to turn the physical device screen on. We can use the POWER button (or Ctrl+p) instead. --- README.md | 1 - app/src/input_manager.c | 7 ++----- app/src/main.c | 3 --- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0ee6d79b..a0bc3914 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,6 @@ you are interested, see [issue 14]. | click on `POWER` | `Ctrl`+`p` | | power on | _Right-click²_ | | turn device screen off (keep mirroring)| `Ctrl`+`o` | - | turn device screen on | `Ctrl`+`Shift`+`o` | | expand notification panel | `Ctrl`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` | | copy device clipboard to computer | `Ctrl`+`c` | diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 10a84e17..03f299db 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -282,11 +282,8 @@ input_manager_process_key(struct input_manager *input_manager, } return; case SDLK_o: - if (control && ctrl && !meta && down) { - enum screen_power_mode mode = shift - ? SCREEN_POWER_MODE_NORMAL - : SCREEN_POWER_MODE_OFF; - set_screen_power_mode(controller, mode); + if (control && ctrl && !shift && !meta && down) { + set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); } return; case SDLK_DOWN: diff --git a/app/src/main.c b/app/src/main.c index d8568884..2bdfb916 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -134,9 +134,6 @@ static void usage(const char *arg0) { " Ctrl+o\n" " turn device screen off (keep mirroring)\n" "\n" - " Ctrl+Shift+o\n" - " turn device screen on\n" - "\n" " Ctrl+n\n" " expand notification panel\n" "\n" From 5b56900e2b2e08631f62f073107499985730ceba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 4 Jun 2019 21:29:14 +0200 Subject: [PATCH 61/90] Rename unused field The flag is used only in the server_start() implementation, there is no need to store it in the structure. --- app/src/server.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/server.h b/app/src/server.h index 0c8443bb..843258be 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -16,7 +16,6 @@ struct server { uint16_t local_port; bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" - bool send_frame_meta; // request frame PTS to be able to record properly }; #define SERVER_INITIALIZER { \ @@ -28,7 +27,6 @@ struct server { .local_port = 0, \ .tunnel_enabled = false, \ .tunnel_forward = false, \ - .send_frame_meta = false, \ } // init default values From c8a6783494b133244da69f7e551de1e25fa66c5a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 4 Jun 2019 21:49:26 +0200 Subject: [PATCH 62/90] Use positive options names internally For clarity, store the flag resulting of the command-line options --no-control and --no-display into "control" and "display". --- app/src/main.c | 4 ++-- app/src/scrcpy.c | 15 ++++++--------- app/src/scrcpy.h | 4 ++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 2bdfb916..24f6b05a 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -455,8 +455,8 @@ main(int argc, char *argv[]) { .show_touches = args.show_touches, .fullscreen = args.fullscreen, .always_on_top = args.always_on_top, - .no_control = args.no_control, - .no_display = args.no_display, + .control = !args.no_control, + .display = !args.no_display, }; int res = scrcpy(&options) ? 0 : 1; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0264561b..7a067134 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -294,10 +294,7 @@ scrcpy(const struct scrcpy_options *options) { bool controller_initialized = false; bool controller_started = false; - bool display = !options->no_display; - bool control = !options->no_control; - - if (!sdl_init_and_configure(display)) { + if (!sdl_init_and_configure(options->display)) { goto end; } @@ -316,13 +313,13 @@ scrcpy(const struct scrcpy_options *options) { } struct decoder *dec = NULL; - if (display) { + if (options->display) { if (!video_buffer_init(&video_buffer)) { goto end; } video_buffer_initialized = true; - if (control) { + if (options->control) { if (!file_handler_init(&file_handler, server.serial)) { goto end; } @@ -356,8 +353,8 @@ scrcpy(const struct scrcpy_options *options) { } stream_started = true; - if (display) { - if (control) { + if (options->display) { + if (options->control) { if (!controller_init(&controller, server.control_socket)) { goto end; } @@ -382,7 +379,7 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } - ret = event_loop(display, control); + ret = event_loop(options->display, options->control); LOGD("quit..."); screen_destroy(&screen); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 114c12a4..52bd14cc 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -16,8 +16,8 @@ struct scrcpy_options { bool show_touches; bool fullscreen; bool always_on_top; - bool no_control; - bool no_display; + bool control; + bool display; }; bool From ca767ba36410bc90532a8b441c3c4712f973c352 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 4 Jun 2019 23:59:55 +0200 Subject: [PATCH 63/90] Group server params in a struct Starting the server requires more and more parameters. For clarity, group them in a struct. --- app/src/scrcpy.c | 11 ++++++++--- app/src/server.c | 30 ++++++++++++------------------ app/src/server.h | 11 +++++++++-- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7a067134..c7b78fee 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -271,9 +271,14 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { bool scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; - if (!server_start(&server, options->serial, options->port, - options->max_size, options->bit_rate, options->crop, - record)) { + struct server_params params = { + .crop = options->crop, + .local_port = options->port, + .max_size = options->max_size, + .bit_rate = options->bit_rate, + .send_frame_meta = record, + }; + if (!server_start(&server, options->serial, ¶ms)) { return false; } diff --git a/app/src/server.c b/app/src/server.c index 3f76e5c7..5b586b93 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -79,14 +79,11 @@ disable_tunnel(struct server *server) { } static process_t -execute_server(const char *serial, - uint16_t max_size, uint32_t bit_rate, - bool tunnel_forward, const char *crop, - bool send_frame_meta) { +execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; - sprintf(max_size_string, "%"PRIu16, max_size); - sprintf(bit_rate_string, "%"PRIu32, bit_rate); + sprintf(max_size_string, "%"PRIu16, params->max_size); + sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); const char *const cmd[] = { "shell", "CLASSPATH=/data/local/tmp/scrcpy-server.jar", @@ -95,11 +92,11 @@ execute_server(const char *serial, "com.genymobile.scrcpy.Server", max_size_string, bit_rate_string, - tunnel_forward ? "true" : "false", - crop ? crop : "-", - send_frame_meta ? "true" : "false", + server->tunnel_forward ? "true" : "false", + params->crop ? params->crop : "-", + params->send_frame_meta ? "true" : "false", }; - return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); + return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } #define IPV4_LOCALHOST 0x7F000001 @@ -160,9 +157,8 @@ server_init(struct server *server) { bool server_start(struct server *server, const char *serial, - uint16_t local_port, uint16_t max_size, uint32_t bit_rate, - const char *crop, bool send_frame_meta) { - server->local_port = local_port; + const struct server_params *params) { + server->local_port = params->local_port; if (serial) { server->serial = SDL_strdup(serial); @@ -191,9 +187,9 @@ server_start(struct server *server, const char *serial, // need to try to connect until the server socket is listening on the // device. - server->server_socket = listen_on_port(local_port); + server->server_socket = listen_on_port(params->local_port); if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, local_port); + LOGE("Could not listen on port %" PRIu16, params->local_port); disable_tunnel(server); SDL_free(server->serial); return false; @@ -201,9 +197,7 @@ server_start(struct server *server, const char *serial, } // server will connect to our server socket - server->process = execute_server(serial, max_size, bit_rate, - server->tunnel_forward, crop, - send_frame_meta); + server->process = execute_server(server, params); if (server->process == PROCESS_NONE) { if (!server->tunnel_forward) { diff --git a/app/src/server.h b/app/src/server.h index 843258be..de0d14b8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -29,6 +29,14 @@ struct server { .tunnel_forward = false, \ } +struct server_params { + const char *crop; + uint16_t local_port; + uint16_t max_size; + uint32_t bit_rate; + bool send_frame_meta; +}; + // init default values void server_init(struct server *server); @@ -36,8 +44,7 @@ server_init(struct server *server); // push, enable tunnel et start the server bool server_start(struct server *server, const char *serial, - uint16_t local_port, uint16_t max_size, uint32_t bit_rate, - const char *crop, bool send_frame_meta); + const struct server_params *params); // block until the communication with the server is established bool From acc4dcd520f960a3e5d8fb431ceb0cbc3e7825e4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 4 Jun 2019 21:31:46 +0200 Subject: [PATCH 64/90] Disable server controller if --no-control If --no-control is disabled, there is no need for a controller. It also avoids to power on the device on start if control is disabled. --- app/src/scrcpy.c | 1 + app/src/server.c | 1 + app/src/server.h | 1 + .../main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../main/java/com/genymobile/scrcpy/Server.java | 15 ++++++++++----- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c7b78fee..e85577a4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -277,6 +277,7 @@ scrcpy(const struct scrcpy_options *options) { .max_size = options->max_size, .bit_rate = options->bit_rate, .send_frame_meta = record, + .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { return false; diff --git a/app/src/server.c b/app/src/server.c index 5b586b93..6c12ba99 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -95,6 +95,7 @@ execute_server(struct server *server, const struct server_params *params) { server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", params->send_frame_meta ? "true" : "false", + params->control ? "true" : "false", }; return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } diff --git a/app/src/server.h b/app/src/server.h index de0d14b8..74a6cac8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -35,6 +35,7 @@ struct server_params { uint16_t max_size; uint32_t bit_rate; bool send_frame_meta; + bool control; }; // init default values diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 851c7ed6..af6b2ee1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -8,6 +8,7 @@ public class Options { private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly + private boolean control; public int getMaxSize() { return maxSize; @@ -48,4 +49,12 @@ public class Options { public void setSendFrameMeta(boolean sendFrameMeta) { this.sendFrameMeta = sendFrameMeta; } + + public boolean getControl() { + return control; + } + + public void setControl(boolean control) { + this.control = control; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 76028fbe..1e4d10d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -19,11 +19,13 @@ public final class Server { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); - Controller controller = new Controller(device, connection); + if (options.getControl()) { + Controller controller = new Controller(device, connection); - // asynchronous - startController(controller); - startDeviceMessageSender(controller.getSender()); + // asynchronous + startController(controller); + startDeviceMessageSender(controller.getSender()); + } try { // synchronous @@ -65,7 +67,7 @@ public final class Server { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { - if (args.length != 5) { + if (args.length != 6) { throw new IllegalArgumentException("Expecting 5 parameters"); } @@ -87,6 +89,9 @@ public final class Server { boolean sendFrameMeta = Boolean.parseBoolean(args[4]); options.setSendFrameMeta(sendFrameMeta); + boolean control = Boolean.parseBoolean(args[5]); + options.setControl(control); + return options; } From 7f07b134468d891f5869bb11a6a60e7c836fd4bb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 5 Jun 2019 00:55:39 +0200 Subject: [PATCH 65/90] Indent command-line options Preparse indentation for --turn-screen-off. --- app/src/main.c | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 24f6b05a..e0008e93 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -286,21 +286,21 @@ guess_record_format(const char *filename) { static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'f'}, - {"serial", required_argument, NULL, 's'}, - {"show-touches", no_argument, NULL, 't'}, - {"version", no_argument, NULL, 'v'}, - {NULL, 0, NULL, 0 }, + {"always-on-top", no_argument, NULL, 'T'}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, 'c'}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"port", required_argument, NULL, 'p'}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, 'f'}, + {"serial", required_argument, NULL, 's'}, + {"show-touches", no_argument, NULL, 't'}, + {"version", no_argument, NULL, 'v'}, + {NULL, 0, NULL, 0 }, }; int c; while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options, From 8e66b33000994f3b95de5128e0f78fdf62f51a5a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 5 Jun 2019 00:55:46 +0200 Subject: [PATCH 66/90] Add option to turn device screen off In addition to the shortcut (Ctrl+o) to turn the device screen off, add a command-line argument to turn it off on start. --- app/src/main.c | 12 +++++++++++- app/src/scrcpy.c | 10 ++++++++++ app/src/scrcpy.h | 1 + .../java/com/genymobile/scrcpy/Controller.java | 18 +++++++++++++----- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index e0008e93..4d4cf5ca 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -28,6 +28,7 @@ struct args { uint16_t max_size; uint32_t bit_rate; bool always_on_top; + bool turn_screen_off; }; static void usage(const char *arg0) { @@ -82,6 +83,9 @@ static void usage(const char *arg0) { " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" "\n" + " -S, --turn-screen-off\n" + " Turn the device screen off immediately.\n" + "\n" " -t, --show-touches\n" " Enable \"show touches\" on start, disable on quit.\n" " It only shows physical touches (not clicks from scrcpy).\n" @@ -299,11 +303,12 @@ parse_args(struct args *args, int argc, char *argv[]) { {"record-format", required_argument, NULL, 'f'}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, {NULL, 0, NULL, 0 }, }; int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options, + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -347,6 +352,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case 's': args->serial = optarg; break; + case 'S': + args->turn_screen_off = true; + break; case 't': args->show_touches = true; break; @@ -417,6 +425,7 @@ main(int argc, char *argv[]) { .always_on_top = false, .no_control = false, .no_display = false, + .turn_screen_off = false, }; if (!parse_args(&args, argc, argv)) { return 1; @@ -457,6 +466,7 @@ main(int argc, char *argv[]) { .always_on_top = args.always_on_top, .control = !args.no_control, .display = !args.no_display, + .turn_screen_off = args.turn_screen_off, }; int res = scrcpy(&options) ? 0 : 1; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e85577a4..0e24fc7c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -375,6 +375,16 @@ scrcpy(const struct scrcpy_options *options) { goto end; } + if (options->turn_screen_off) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; + + if (!controller_push_msg(&controller, &msg)) { + LOGW("Cannot request 'set screen power mode'"); + } + } + if (options->fullscreen) { screen_switch_fullscreen(&screen); } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 52bd14cc..4cbadcad 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -18,6 +18,7 @@ struct scrcpy_options { bool always_on_top; bool control; bool display; + bool turn_screen_off; }; bool diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index a43ee5ba..263fc2fc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -53,9 +53,21 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); } + @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device - powerOn(); + if (!device.isScreenOn()) { + injectKeycode(KeyEvent.KEYCODE_POWER); + + // dirty hack + // After POWER 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. + // Adding this delay prevents to handle the message before the device is actually + // powered on. + SystemClock.sleep(500); + } while (true) { handleEvent(); @@ -182,10 +194,6 @@ public class Controller { return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } - private boolean powerOn() { - return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER); - } - private boolean pressBackOrTurnScreenOn() { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; return injectKeycode(keycode); From 92539968739ea8ba58af3f19541b2b9c4e89c7e2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 5 Jun 2019 19:01:42 +0200 Subject: [PATCH 67/90] Add README section explaining --turn-screen-off --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index a0bc3914..2dcb3434 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,20 @@ scrcpy --no-control scrcpy -n ``` +### Turn screen off + +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Or by pressing `Ctrl`+`o` at any time. + +To turn it back on, press `POWER` (or `Ctrl`+`p`). + ### Forward audio From a143b8b07af14c579300ff536fc195e9351c9226 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 5 Jun 2019 18:47:09 +0200 Subject: [PATCH 68/90] Indent command-line options Prepare indentation for --render-expired-frames. --- app/src/main.c | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 4d4cf5ca..09ea107c 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -290,22 +290,22 @@ guess_record_format(const char *filename) { static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, - {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"max-size", required_argument, NULL, 'm'}, - {"no-control", no_argument, NULL, 'n'}, - {"no-display", no_argument, NULL, 'N'}, - {"port", required_argument, NULL, 'p'}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'f'}, - {"serial", required_argument, NULL, 's'}, - {"show-touches", no_argument, NULL, 't'}, - {"turn-screen-off", no_argument, NULL, 'S'}, - {"version", no_argument, NULL, 'v'}, - {NULL, 0, NULL, 0 }, + {"always-on-top", no_argument, NULL, 'T'}, + {"bit-rate", required_argument, NULL, 'b'}, + {"crop", required_argument, NULL, 'c'}, + {"fullscreen", no_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"max-size", required_argument, NULL, 'm'}, + {"no-control", no_argument, NULL, 'n'}, + {"no-display", no_argument, NULL, 'N'}, + {"port", required_argument, NULL, 'p'}, + {"record", required_argument, NULL, 'r'}, + {"record-format", required_argument, NULL, 'f'}, + {"serial", required_argument, NULL, 's'}, + {"show-touches", no_argument, NULL, 't'}, + {"turn-screen-off", no_argument, NULL, 'S'}, + {"version", no_argument, NULL, 'v'}, + {NULL, 0, NULL, 0 }, }; int c; while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, From ebccb9f6cc111e8acfbe10d656cac5c1f1b744a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 5 Jun 2019 19:02:50 +0200 Subject: [PATCH 69/90] Add runtime option to render expired frames Replace the compilation flag SKIP_FRAMES by a runtime flag to force rendering of expired frames. By default, the expired frames are skipped. --- README.md | 13 ++++++++ app/meson.build | 5 ---- app/src/fps_counter.c | 12 +------- app/src/fps_counter.h | 6 ---- app/src/main.c | 16 ++++++++++ app/src/scrcpy.c | 2 +- app/src/scrcpy.h | 1 + app/src/video_buffer.c | 67 +++++++++++++++++++++--------------------- app/src/video_buffer.h | 6 ++-- 9 files changed, 67 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 2dcb3434..10f6138e 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,19 @@ Or by pressing `Ctrl`+`o` at any time. To turn it back on, press `POWER` (or `Ctrl`+`p`). +### Render expired frames + +By default, to minimize latency, _scrcpy_ always renders the last decoded frame +available, and drops any previous one. + +To force the rendering of all frames (at a cost of a possible increased +latency), use: + +```bash +scrcpy --render-expired-frames +``` + + ### Forward audio Audio is not forwarded by _scrcpy_. diff --git a/app/meson.build b/app/meson.build index d47eda79..732cf1ee 100644 --- a/app/meson.build +++ b/app/meson.build @@ -122,11 +122,6 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps -# whether the app should always display the most recent available frame, even -# if the previous one has not been displayed -# SKIP_FRAMES improves latency at the cost of framerate -conf.set('SKIP_FRAMES', get_option('skip_frames')) - # enable High DPI support conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 6c8ef795..133c7a25 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -16,9 +16,7 @@ fps_counter_start(struct fps_counter *counter) { counter->started = true; counter->slice_start = SDL_GetTicks(); counter->nr_rendered = 0; -#ifdef SKIP_FRAMES counter->nr_skipped = 0; -#endif } void @@ -28,16 +26,12 @@ fps_counter_stop(struct fps_counter *counter) { static void display_fps(struct fps_counter *counter) { -#ifdef SKIP_FRAMES if (counter->nr_skipped) { LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, counter->nr_skipped); } else { -#endif - LOGI("%d fps", counter->nr_rendered); -#ifdef SKIP_FRAMES + LOGI("%d fps", counter->nr_rendered); } -#endif } static void @@ -49,9 +43,7 @@ check_expired(struct fps_counter *counter) { uint32_t elapsed_slices = (now - counter->slice_start) / 1000; counter->slice_start += 1000 * elapsed_slices; counter->nr_rendered = 0; -#ifdef SKIP_FRAMES counter->nr_skipped = 0; -#endif } } @@ -61,10 +53,8 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { ++counter->nr_rendered; } -#ifdef SKIP_FRAMES void fps_counter_add_skipped_frame(struct fps_counter *counter) { check_expired(counter); ++counter->nr_skipped; } -#endif diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index dcdf10bf..fecef806 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -4,15 +4,11 @@ #include #include -#include "config.h" - struct fps_counter { bool started; uint32_t slice_start; // initialized by SDL_GetTicks() int nr_rendered; -#ifdef SKIP_FRAMES int nr_skipped; -#endif }; void @@ -27,9 +23,7 @@ fps_counter_stop(struct fps_counter *counter); void fps_counter_add_rendered_frame(struct fps_counter *counter); -#ifdef SKIP_FRAMES void fps_counter_add_skipped_frame(struct fps_counter *counter); -#endif #endif diff --git a/app/src/main.c b/app/src/main.c index 09ea107c..bf3b7a50 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -29,6 +29,7 @@ struct args { uint32_t bit_rate; bool always_on_top; bool turn_screen_off; + bool render_expired_frames; }; static void usage(const char *arg0) { @@ -79,6 +80,12 @@ static void usage(const char *arg0) { " The format is determined by the -F/--record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --render-expired-frames\n" + " By default, to minimize latency, scrcpy always renders the\n" + " last available decoded frame, and drops any previous ones.\n" + " This flag forces to render all frames, at a cost of a\n" + " possible increased latency.\n" + "\n" " -s, --serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" @@ -287,6 +294,8 @@ guess_record_format(const char *filename) { return 0; } +#define OPT_RENDER_EXPIRED_FRAMES 1000 + static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { @@ -301,6 +310,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"port", required_argument, NULL, 'p'}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, 'f'}, + {"render-expired-frames", no_argument, NULL, + OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, @@ -364,6 +375,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case 'v': args->version = true; break; + case OPT_RENDER_EXPIRED_FRAMES: + args->render_expired_frames = true; + break; default: // getopt prints the error message on stderr return false; @@ -426,6 +440,7 @@ main(int argc, char *argv[]) { .no_control = false, .no_display = false, .turn_screen_off = false, + .render_expired_frames = false, }; if (!parse_args(&args, argc, argv)) { return 1; @@ -467,6 +482,7 @@ main(int argc, char *argv[]) { .control = !args.no_control, .display = !args.no_display, .turn_screen_off = args.turn_screen_off, + .render_expired_frames = args.render_expired_frames, }; int res = scrcpy(&options) ? 0 : 1; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0e24fc7c..f5041902 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -320,7 +320,7 @@ scrcpy(const struct scrcpy_options *options) { struct decoder *dec = NULL; if (options->display) { - if (!video_buffer_init(&video_buffer)) { + if (!video_buffer_init(&video_buffer, options->render_expired_frames)) { goto end; } video_buffer_initialized = true; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 4cbadcad..d705d2db 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -19,6 +19,7 @@ struct scrcpy_options { bool control; bool display; bool turn_screen_off; + bool render_expired_frames; }; bool diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 8f1d1d9d..f26af0b6 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -10,7 +10,7 @@ #include "log.h" bool -video_buffer_init(struct video_buffer *vb) { +video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { if (!(vb->decoding_frame = av_frame_alloc())) { goto error_0; } @@ -23,13 +23,16 @@ video_buffer_init(struct video_buffer *vb) { goto error_2; } -#ifndef SKIP_FRAMES - if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { - SDL_DestroyMutex(vb->mutex); - goto error_2; + vb->render_expired_frames = render_expired_frames; + if (render_expired_frames) { + if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { + SDL_DestroyMutex(vb->mutex); + goto error_2; + } + // interrupted is not used if expired frames are not rendered + // since offering a frame will never block + vb->interrupted = false; } - vb->interrupted = false; -#endif // there is initially no rendering frame, so consider it has already been // consumed @@ -48,9 +51,9 @@ error_0: void video_buffer_destroy(struct video_buffer *vb) { -#ifndef SKIP_FRAMES - SDL_DestroyCond(vb->rendering_frame_consumed_cond); -#endif + if (vb->render_expired_frames) { + SDL_DestroyCond(vb->rendering_frame_consumed_cond); + } SDL_DestroyMutex(vb->mutex); av_frame_free(&vb->rendering_frame); av_frame_free(&vb->decoding_frame); @@ -67,17 +70,16 @@ void video_buffer_offer_decoded_frame(struct video_buffer *vb, bool *previous_frame_skipped) { mutex_lock(vb->mutex); -#ifndef SKIP_FRAMES - // if SKIP_FRAMES is disabled, then the decoder must wait for the current - // frame to be consumed - while (!vb->rendering_frame_consumed && !vb->interrupted) { - cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); + if (vb->render_expired_frames) { + // wait for the current (expired) frame to be consumed + while (!vb->rendering_frame_consumed && !vb->interrupted) { + cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); + } + } else { + if (vb->fps_counter.started && !vb->rendering_frame_consumed) { + fps_counter_add_skipped_frame(&vb->fps_counter); + } } -#else - if (vb->fps_counter.started && !vb->rendering_frame_consumed) { - fps_counter_add_skipped_frame(&vb->fps_counter); - } -#endif video_buffer_swap_frames(vb); @@ -94,23 +96,20 @@ video_buffer_consume_rendered_frame(struct video_buffer *vb) { if (vb->fps_counter.started) { fps_counter_add_rendered_frame(&vb->fps_counter); } -#ifndef SKIP_FRAMES - // if SKIP_FRAMES is disabled, then notify the decoder the current frame is - // consumed, so that it may push a new one - cond_signal(vb->rendering_frame_consumed_cond); -#endif + if (vb->render_expired_frames) { + // unblock video_buffer_offer_decoded_frame() + cond_signal(vb->rendering_frame_consumed_cond); + } return vb->rendering_frame; } void video_buffer_interrupt(struct video_buffer *vb) { -#ifdef SKIP_FRAMES - (void) vb; // unused -#else - mutex_lock(vb->mutex); - vb->interrupted = true; - mutex_unlock(vb->mutex); - // wake up blocking wait - cond_signal(vb->rendering_frame_consumed_cond); -#endif + if (vb->render_expired_frames) { + mutex_lock(vb->mutex); + vb->interrupted = true; + mutex_unlock(vb->mutex); + // wake up blocking wait + cond_signal(vb->rendering_frame_consumed_cond); + } } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 93222236..9f328172 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -4,7 +4,6 @@ #include #include -#include "config.h" #include "fps_counter.h" // forward declarations @@ -14,16 +13,15 @@ struct video_buffer { AVFrame *decoding_frame; AVFrame *rendering_frame; SDL_mutex *mutex; -#ifndef SKIP_FRAMES + bool render_expired_frames; bool interrupted; SDL_cond *rendering_frame_consumed_cond; -#endif bool rendering_frame_consumed; struct fps_counter fps_counter; }; bool -video_buffer_init(struct video_buffer *vb); +video_buffer_init(struct video_buffer *vb, bool render_expired_frames); void video_buffer_destroy(struct video_buffer *vb); From eda44b6068dc44104bb7e1700bc4abfb55d873be Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Jun 2019 00:03:21 +0200 Subject: [PATCH 70/90] Fix controller cleanup After commit bfb86ca2c23c29cf768c9216dbfff3a89d5d0b5f, the controller was not stopped and destroyed on quit. --- app/src/scrcpy.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f5041902..38ad1ceb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -364,10 +364,12 @@ scrcpy(const struct scrcpy_options *options) { if (!controller_init(&controller, server.control_socket)) { goto end; } + controller_initialized = true; if (!controller_start(&controller)) { goto end; } + controller_started = true; } if (!screen_init_rendering(&screen, device_name, frame_size, From d104d3bda91b018c25bf0fdcf1733bc90cc894cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Jun 2019 16:54:31 +0200 Subject: [PATCH 71/90] Add cond_wait_timeout() Add a "timed out" version of cond_wait(). --- app/src/lock_util.c | 10 ++++++++++ app/src/lock_util.h | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/app/src/lock_util.c b/app/src/lock_util.c index 7b70ba6b..36706063 100644 --- a/app/src/lock_util.c +++ b/app/src/lock_util.c @@ -28,6 +28,16 @@ cond_wait(SDL_cond *cond, SDL_mutex *mutex) { } } +int +cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond, mutex, ms); + if (r < 0) { + LOGC("Could not wait on condition with timeout"); + abort(); + } + return r; +} + void cond_signal(SDL_cond *cond) { if (SDL_CondSignal(cond)) { diff --git a/app/src/lock_util.h b/app/src/lock_util.h index 99c1f8d6..6c27602d 100644 --- a/app/src/lock_util.h +++ b/app/src/lock_util.h @@ -1,6 +1,8 @@ #ifndef LOCKUTIL_H #define LOCKUTIL_H +#include + // forward declarations typedef struct SDL_mutex SDL_mutex; typedef struct SDL_cond SDL_cond; @@ -14,6 +16,10 @@ mutex_unlock(SDL_mutex *mutex); void cond_wait(SDL_cond *cond, SDL_mutex *mutex); +// return 0 or SDL_MUTEX_TIMEDOUT +int +cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms); + void cond_signal(SDL_cond *cond); From e2a272bf99ecf48fcb050177113f903b3fb323c4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Jun 2019 16:55:19 +0200 Subject: [PATCH 72/90] Improve framerate counting The FPS counter was called only on new frames, so it could not print values regularly, especially when there are very few FPS (when the device surface does not change). To the extreme, it was never able to display 0 fps. Add a separate thread to print framerate every second. --- app/src/fps_counter.c | 161 +++++++++++++++++++++++++++++++++------- app/src/fps_counter.h | 36 +++++++-- app/src/input_manager.c | 21 ++++-- app/src/scrcpy.c | 18 ++++- app/src/video_buffer.c | 16 ++-- app/src/video_buffer.h | 5 +- 6 files changed, 206 insertions(+), 51 deletions(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 133c7a25..daece470 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,60 +1,169 @@ #include "fps_counter.h" +#include #include +#include "lock_util.h" #include "log.h" -void +#define FPS_COUNTER_INTERVAL_MS 1000 + +bool fps_counter_init(struct fps_counter *counter) { - counter->started = false; - // no need to initialize the other fields, they are meaningful only when - // started is true -} + counter->mutex = SDL_CreateMutex(); + if (!counter->mutex) { + return false; + } -void -fps_counter_start(struct fps_counter *counter) { - counter->started = true; - counter->slice_start = SDL_GetTicks(); - counter->nr_rendered = 0; - counter->nr_skipped = 0; + counter->state_cond = SDL_CreateCond(); + if (!counter->state_cond) { + SDL_DestroyMutex(counter->mutex); + return false; + } + + counter->thread = NULL; + SDL_AtomicSet(&counter->started, 0); + // no need to initialize the other fields, they are unused until started + + return true; } void -fps_counter_stop(struct fps_counter *counter) { - counter->started = false; +fps_counter_destroy(struct fps_counter *counter) { + SDL_DestroyCond(counter->state_cond); + SDL_DestroyMutex(counter->mutex); } +// must be called with mutex locked static void display_fps(struct fps_counter *counter) { + unsigned rendered_per_second = + counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS; if (counter->nr_skipped) { - LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, + LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); } else { - LOGI("%d fps", counter->nr_rendered); + LOGI("%u fps", rendered_per_second); } } +// must be called with mutex locked static void -check_expired(struct fps_counter *counter) { - uint32_t now = SDL_GetTicks(); - if (now - counter->slice_start >= 1000) { - display_fps(counter); - // add a multiple of one second - uint32_t elapsed_slices = (now - counter->slice_start) / 1000; - counter->slice_start += 1000 * elapsed_slices; - counter->nr_rendered = 0; - counter->nr_skipped = 0; +check_interval_expired(struct fps_counter *counter, uint32_t now) { + if (now < counter->next_timestamp) { + return; + } + + display_fps(counter); + counter->nr_rendered = 0; + counter->nr_skipped = 0; + // add a multiple of the interval + uint32_t elapsed_slices = + (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1; + counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices; +} + +static int +run_fps_counter(void *data) { + struct fps_counter *counter = data; + + mutex_lock(counter->mutex); + while (!counter->interrupted) { + while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { + cond_wait(counter->state_cond, counter->mutex); + } + while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + uint32_t now = SDL_GetTicks(); + check_interval_expired(counter, now); + + SDL_assert(counter->next_timestamp > now); + uint32_t remaining = counter->next_timestamp - now; + + // ignore the reason (timeout or signaled), we just loop anyway + cond_wait_timeout(counter->state_cond, counter->mutex, remaining); + } + } + mutex_unlock(counter->mutex); + return 0; +} + +bool +fps_counter_start(struct fps_counter *counter) { + mutex_lock(counter->mutex); + counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; + counter->nr_rendered = 0; + counter->nr_skipped = 0; + mutex_unlock(counter->mutex); + + SDL_AtomicSet(&counter->started, 1); + cond_signal(counter->state_cond); + + // counter->thread is always accessed from the same thread, no need to lock + if (!counter->thread) { + counter->thread = + SDL_CreateThread(run_fps_counter, "fps counter", counter); + if (!counter->thread) { + LOGE("Could not start FPS counter thread"); + return false; + } + } + + return true; +} + +void +fps_counter_stop(struct fps_counter *counter) { + SDL_AtomicSet(&counter->started, 0); + cond_signal(counter->state_cond); +} + +bool +fps_counter_is_started(struct fps_counter *counter) { + return SDL_AtomicGet(&counter->started); +} + +void +fps_counter_interrupt(struct fps_counter *counter) { + if (!counter->thread) { + return; + } + + mutex_lock(counter->mutex); + counter->interrupted = true; + mutex_unlock(counter->mutex); + // wake up blocking wait + cond_signal(counter->state_cond); +} + +void +fps_counter_join(struct fps_counter *counter) { + if (counter->thread) { + SDL_WaitThread(counter->thread, NULL); } } void fps_counter_add_rendered_frame(struct fps_counter *counter) { - check_expired(counter); + if (!SDL_AtomicGet(&counter->started)) { + return; + } + + mutex_lock(counter->mutex); + uint32_t now = SDL_GetTicks(); + check_interval_expired(counter, now); ++counter->nr_rendered; + mutex_unlock(counter->mutex); } void fps_counter_add_skipped_frame(struct fps_counter *counter) { - check_expired(counter); + if (!SDL_AtomicGet(&counter->started)) { + return; + } + + mutex_lock(counter->mutex); + uint32_t now = SDL_GetTicks(); + check_interval_expired(counter, now); ++counter->nr_skipped; + mutex_unlock(counter->mutex); } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index fecef806..6b560a35 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -3,23 +3,49 @@ #include #include +#include +#include +#include struct fps_counter { - bool started; - uint32_t slice_start; // initialized by SDL_GetTicks() - int nr_rendered; - int nr_skipped; + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *state_cond; + + // atomic so that we can check without locking the mutex + // if the FPS counter is disabled, we don't want to lock unnecessarily + SDL_atomic_t started; + + // the following fields are protected by the mutex + bool interrupted; + unsigned nr_rendered; + unsigned nr_skipped; + uint32_t next_timestamp; }; -void +bool fps_counter_init(struct fps_counter *counter); void +fps_counter_destroy(struct fps_counter *counter); + +bool fps_counter_start(struct fps_counter *counter); void fps_counter_stop(struct fps_counter *counter); +bool +fps_counter_is_started(struct fps_counter *counter); + +// request to stop the thread (on quit) +// must be called before fps_counter_join() +void +fps_counter_interrupt(struct fps_counter *counter); + +void +fps_counter_join(struct fps_counter *counter); + void fps_counter_add_rendered_frame(struct fps_counter *counter); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 03f299db..fb8ef8f0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -172,16 +172,19 @@ set_screen_power_mode(struct controller *controller, } static void -switch_fps_counter_state(struct video_buffer *vb) { - mutex_lock(vb->mutex); - if (vb->fps_counter.started) { +switch_fps_counter_state(struct fps_counter *fps_counter) { + // the started state can only be written from the current thread, so there + // is no ToCToU issue + if (fps_counter_is_started(fps_counter)) { + fps_counter_stop(fps_counter); LOGI("FPS counter stopped"); - fps_counter_stop(&vb->fps_counter); } else { - LOGI("FPS counter started"); - fps_counter_start(&vb->fps_counter); + if (fps_counter_start(fps_counter)) { + LOGI("FPS counter started"); + } else { + LOGE("FPS counter starting failed"); + } } - mutex_unlock(vb->mutex); } static void @@ -339,7 +342,9 @@ input_manager_process_key(struct input_manager *input_manager, return; case SDLK_i: if (ctrl && !meta && !shift && !repeat && down) { - switch_fps_counter_state(input_manager->video_buffer); + struct fps_counter *fps_counter = + input_manager->video_buffer->fps_counter; + switch_fps_counter_state(fps_counter); } return; case SDLK_n: diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 38ad1ceb..761edb69 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -29,6 +29,7 @@ static struct server server = SERVER_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER; +static struct fps_counter fps_counter; static struct video_buffer video_buffer; static struct stream stream; static struct decoder decoder; @@ -293,6 +294,7 @@ scrcpy(const struct scrcpy_options *options) { bool ret = false; + bool fps_counter_initialized = false; bool video_buffer_initialized = false; bool file_handler_initialized = false; bool recorder_initialized = false; @@ -320,7 +322,13 @@ scrcpy(const struct scrcpy_options *options) { struct decoder *dec = NULL; if (options->display) { - if (!video_buffer_init(&video_buffer, options->render_expired_frames)) { + if (!fps_counter_init(&fps_counter)) { + goto end; + } + fps_counter_initialized = true; + + if (!video_buffer_init(&video_buffer, &fps_counter, + options->render_expired_frames)) { goto end; } video_buffer_initialized = true; @@ -414,6 +422,9 @@ end: if (file_handler_initialized) { file_handler_stop(&file_handler); } + if (fps_counter_initialized) { + fps_counter_interrupt(&fps_counter); + } // shutdown the sockets and kill the server server_stop(&server); @@ -443,6 +454,11 @@ end: video_buffer_destroy(&video_buffer); } + if (fps_counter_initialized) { + fps_counter_join(&fps_counter); + fps_counter_destroy(&fps_counter); + } + if (options->show_touches) { if (!show_touches_waited) { // wait the process which enabled "show touches" diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index f26af0b6..2b5f1c2f 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -10,7 +10,10 @@ #include "log.h" bool -video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { +video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, + bool render_expired_frames) { + vb->fps_counter = fps_counter; + if (!(vb->decoding_frame = av_frame_alloc())) { goto error_0; } @@ -37,7 +40,6 @@ video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { // there is initially no rendering frame, so consider it has already been // consumed vb->rendering_frame_consumed = true; - fps_counter_init(&vb->fps_counter); return true; @@ -75,10 +77,8 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, while (!vb->rendering_frame_consumed && !vb->interrupted) { cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); } - } else { - if (vb->fps_counter.started && !vb->rendering_frame_consumed) { - fps_counter_add_skipped_frame(&vb->fps_counter); - } + } else if (!vb->rendering_frame_consumed) { + fps_counter_add_skipped_frame(vb->fps_counter); } video_buffer_swap_frames(vb); @@ -93,9 +93,7 @@ const AVFrame * video_buffer_consume_rendered_frame(struct video_buffer *vb) { SDL_assert(!vb->rendering_frame_consumed); vb->rendering_frame_consumed = true; - if (vb->fps_counter.started) { - fps_counter_add_rendered_frame(&vb->fps_counter); - } + fps_counter_add_rendered_frame(vb->fps_counter); if (vb->render_expired_frames) { // unblock video_buffer_offer_decoded_frame() cond_signal(vb->rendering_frame_consumed_cond); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 9f328172..26a6fa1f 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -17,11 +17,12 @@ struct video_buffer { bool interrupted; SDL_cond *rendering_frame_consumed_cond; bool rendering_frame_consumed; - struct fps_counter fps_counter; + struct fps_counter *fps_counter; }; bool -video_buffer_init(struct video_buffer *vb, bool render_expired_frames); +video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, + bool render_expired_frames); void video_buffer_destroy(struct video_buffer *vb); From 5d1133925994077d93d37e95565c359e026abfff Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Jun 2019 17:19:00 +0200 Subject: [PATCH 73/90] Inline lock_util functions They are just tiny wrappers. --- app/meson.build | 1 - app/src/lock_util.c | 48 --------------------------------- app/src/lock_util.h | 65 +++++++++++++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 69 deletions(-) delete mode 100644 app/src/lock_util.c diff --git a/app/meson.build b/app/meson.build index 732cf1ee..7781d776 100644 --- a/app/meson.build +++ b/app/meson.build @@ -10,7 +10,6 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', - 'src/lock_util.c', 'src/net.c', 'src/receiver.c', 'src/recorder.c', diff --git a/app/src/lock_util.c b/app/src/lock_util.c deleted file mode 100644 index 36706063..00000000 --- a/app/src/lock_util.c +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include -#include - -#include "log.h" - -void -mutex_lock(SDL_mutex *mutex) { - if (SDL_LockMutex(mutex)) { - LOGC("Could not lock mutex"); - abort(); - } -} - -void -mutex_unlock(SDL_mutex *mutex) { - if (SDL_UnlockMutex(mutex)) { - LOGC("Could not unlock mutex"); - abort(); - } -} - -void -cond_wait(SDL_cond *cond, SDL_mutex *mutex) { - if (SDL_CondWait(cond, mutex)) { - LOGC("Could not wait on condition"); - abort(); - } -} - -int -cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { - int r = SDL_CondWaitTimeout(cond, mutex, ms); - if (r < 0) { - LOGC("Could not wait on condition with timeout"); - abort(); - } - return r; -} - -void -cond_signal(SDL_cond *cond) { - if (SDL_CondSignal(cond)) { - LOGC("Could not signal a condition"); - abort(); - } -} - diff --git a/app/src/lock_util.h b/app/src/lock_util.h index 6c27602d..d1ca7336 100644 --- a/app/src/lock_util.h +++ b/app/src/lock_util.h @@ -2,25 +2,50 @@ #define LOCKUTIL_H #include - -// forward declarations -typedef struct SDL_mutex SDL_mutex; -typedef struct SDL_cond SDL_cond; - -void -mutex_lock(SDL_mutex *mutex); - -void -mutex_unlock(SDL_mutex *mutex); - -void -cond_wait(SDL_cond *cond, SDL_mutex *mutex); - -// return 0 or SDL_MUTEX_TIMEDOUT -int -cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms); - -void -cond_signal(SDL_cond *cond); +#include + +#include "log.h" + +static inline void +mutex_lock(SDL_mutex *mutex) { + if (SDL_LockMutex(mutex)) { + LOGC("Could not lock mutex"); + abort(); + } +} + +static inline void +mutex_unlock(SDL_mutex *mutex) { + if (SDL_UnlockMutex(mutex)) { + LOGC("Could not unlock mutex"); + abort(); + } +} + +static inline void +cond_wait(SDL_cond *cond, SDL_mutex *mutex) { + if (SDL_CondWait(cond, mutex)) { + LOGC("Could not wait on condition"); + abort(); + } +} + +static inline int +cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond, mutex, ms); + if (r < 0) { + LOGC("Could not wait on condition with timeout"); + abort(); + } + return r; +} + +static inline void +cond_signal(SDL_cond *cond) { + if (SDL_CondSignal(cond)) { + LOGC("Could not signal a condition"); + abort(); + } +} #endif From 8604f16b30892e7dc2482d221f611757aefb0d87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Jun 2019 17:45:03 +0200 Subject: [PATCH 74/90] Truncate device name at UTF-8 code point boundary Just in case. --- .../src/main/java/com/genymobile/scrcpy/DesktopConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index a26c74cd..a725d83d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -89,7 +89,7 @@ public final class DesktopConnection implements Closeable { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); - int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length); + int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1); System.arraycopy(deviceNameBytes, 0, buffer, 0, len); // byte[] are always 0-initialized in java, no need to set '\0' explicitly From 72bdfbc7a6f0a60358eb3976925ebb6a4d8ad08e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 17:39:50 +0200 Subject: [PATCH 75/90] Never return 0 for stream protocol On socket disconnection, on Linux, recv() returns -1 and errno is set. But on Windows, errno is 0. In that case, AVERROR(errno) == 0, leading to the warning: > Invalid return value 0 for stream protocol To avoid the problem, if errno is 0, return AVERROR_EOF. Ref: commit 2876463d394c3bc4dc239a605e65ebab75598353 --- app/src/stream.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/stream.c b/app/src/stream.c index 9a946375..4f38cecf 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -113,7 +113,7 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { ssize_t r = net_recv(stream->socket, buf, buf_size); if (r == -1) { - return AVERROR(errno); + return errno ? AVERROR(errno) : AVERROR_EOF; } if (r == 0) { return AVERROR_EOF; @@ -130,7 +130,7 @@ read_raw_packet(void *opaque, uint8_t *buf, int buf_size) { struct stream *stream = opaque; ssize_t r = net_recv(stream->socket, buf, buf_size); if (r == -1) { - return AVERROR(errno); + return errno ? AVERROR(errno) : AVERROR_EOF; } if (r == 0) { return AVERROR_EOF; From b777760bcac71f59797e69600448b814b215b618 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 8 Jun 2019 19:44:03 +0200 Subject: [PATCH 76/90] Simplify scrcpy-server path configuration The full path of scrcpy-server.jar was partially configured from meson.build then concatenated by C code. Instead, directly write the path in C. --- app/meson.build | 5 ----- app/src/server.c | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/meson.build b/app/meson.build index 7781d776..37e2372a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,11 +93,6 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# the path of the server, which will be appended to the prefix -# ignored if OVERRIDE_SERVER_PATH if defined -# must be consistent with the install_dir in server/meson.build -conf.set_quoted('PREFIXED_SERVER_PATH', '/share/scrcpy/scrcpy-server.jar') - # the path of the server to be used "as is" # this is useful for building a "portable" version (with the server in the same # directory as the client) diff --git a/app/src/server.c b/app/src/server.c index 6c12ba99..6dbf6c76 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -15,7 +15,7 @@ #ifdef OVERRIDE_SERVER_PATH # define DEFAULT_SERVER_PATH OVERRIDE_SERVER_PATH #else -# define DEFAULT_SERVER_PATH PREFIX PREFIXED_SERVER_PATH +# define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/scrcpy-server.jar" #endif #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" From eb34098add981aebd0c9aaed89ea9fa04d9ce798 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 8 Jun 2019 19:03:22 +0200 Subject: [PATCH 77/90] Simplify portable build configuration To create a portable build (with scrcpy-server.jar accessible from the scrcpy directory), replace OVERRIDE_SERVER_PATH by a simple compilation flag: PORTABLE. This paves the way to use more complex rules to determine the path of scrcpy-server.jar in portable builds. --- Makefile.CrossWindows | 8 ++++---- app/meson.build | 13 +++---------- app/src/server.c | 4 ++-- meson_options.txt | 2 +- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 0f7b14a6..960983df 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -57,7 +57,7 @@ build-win32: prepare-deps-win32 --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ -Dbuild_server=false \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" build-win32-noconsole: prepare-deps-win32 @@ -68,7 +68,7 @@ build-win32-noconsole: prepare-deps-win32 -Dcrossbuild_windows=true \ -Dbuild_server=false \ -Dwindows_noconsole=true \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" prepare-deps-win64: @@ -81,7 +81,7 @@ build-win64: prepare-deps-win64 --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ -Dbuild_server=false \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" build-win64-noconsole: prepare-deps-win64 @@ -92,7 +92,7 @@ build-win64-noconsole: prepare-deps-win64 -Dcrossbuild_windows=true \ -Dbuild_server=false \ -Dwindows_noconsole=true \ - -Doverride_server_path=scrcpy-server.jar ) + -Dportable=true ) ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" dist-win32: build-server build-win32 build-win32-noconsole diff --git a/app/meson.build b/app/meson.build index 37e2372a..673892cd 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,16 +93,9 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# the path of the server to be used "as is" -# this is useful for building a "portable" version (with the server in the same -# directory as the client) -override_server_path = get_option('override_server_path') -if override_server_path != '' - conf.set_quoted('OVERRIDE_SERVER_PATH', override_server_path) -else - # undefine it - conf.set('OVERRIDE_SERVER_PATH', false) -endif +# build a "portable" version (with scrcpy-server.jar accessible from the +# current directory) +conf.set('PORTABLE', get_option('portable')) # the default client TCP port for the "adb reverse" tunnel # overridden by option --port diff --git a/app/src/server.c b/app/src/server.c index 6dbf6c76..47e0780f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -12,8 +12,8 @@ #define SOCKET_NAME "scrcpy" -#ifdef OVERRIDE_SERVER_PATH -# define DEFAULT_SERVER_PATH OVERRIDE_SERVER_PATH +#ifdef PORTABLE +# define DEFAULT_SERVER_PATH "scrcpy-server.jar" #else # define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/scrcpy-server.jar" #endif diff --git a/meson_options.txt b/meson_options.txt index 567f0a39..90b4293c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,6 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('override_server_path', type: 'string', description: 'Hardcoded path to find the server at runtime') +option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the current directory') option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') From 4eb6b26c93e6af6f44f125244a04c0a343b3b996 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Jun 2019 15:57:10 +0200 Subject: [PATCH 78/90] Extract "scrcpy-server.jar" string The filename is used at several places. --- app/src/server.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 47e0780f..e7c8712e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -11,14 +11,15 @@ #include "net.h" #define SOCKET_NAME "scrcpy" +#define SERVER_FILENAME "scrcpy-server.jar" #ifdef PORTABLE -# define DEFAULT_SERVER_PATH "scrcpy-server.jar" +# define DEFAULT_SERVER_PATH SERVER_FILENAME #else -# define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/scrcpy-server.jar" +# define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #endif -#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME static const char * get_server_path(void) { @@ -86,7 +87,7 @@ execute_server(struct server *server, const struct server_params *params) { sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); const char *const cmd[] = { "shell", - "CLASSPATH=/data/local/tmp/scrcpy-server.jar", + "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "app_process", "/", // unused "com.genymobile.scrcpy.Server", From 3b17ff7c866cf6b27ce82dbe6a8f87df1310069c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Jun 2019 15:44:45 +0200 Subject: [PATCH 79/90] Add functions to convert wide char to UTF-8 There was already utf8_to_wide_char(), used to correctly execute commands on Windows. Add the reverse converter: utf8_from_wide_char(). We will need it to build the scrcpy-server path based on the executable directory. --- app/src/str_util.c | 16 ++++++++++++++++ app/src/str_util.h | 3 +++ 2 files changed, 19 insertions(+) diff --git a/app/src/str_util.c b/app/src/str_util.c index 2878bf96..7d46a1a0 100644 --- a/app/src/str_util.c +++ b/app/src/str_util.c @@ -92,4 +92,20 @@ utf8_to_wide_char(const char *utf8) { return wide; } +char * +utf8_from_wide_char(const wchar_t *ws) { + int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + if (!len) { + return NULL; + } + + char *utf8 = SDL_malloc(len); + if (!utf8) { + return NULL; + } + + WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL); + return utf8; +} + #endif diff --git a/app/src/str_util.h b/app/src/str_util.h index 0d1b9c01..0b7a571a 100644 --- a/app/src/str_util.h +++ b/app/src/str_util.h @@ -32,6 +32,9 @@ utf8_truncation_index(const char *utf8, size_t max_len); // returns the new allocated string, to be freed by the caller wchar_t * utf8_to_wide_char(const char *utf8); + +char * +utf8_from_wide_char(const wchar_t *s); #endif #endif From 2755bfc255f28db0727e29749d456eebe82ba776 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Jun 2019 15:14:10 +0200 Subject: [PATCH 80/90] Improve portable builds In portable builds, scrcpy-server.jar was supposed to be present in the current directory, so in practice it worked only if scrcpy was launched from its own directory. Instead, find the absolute path of the executable and build a suitable path to use scrcpy-server.jar from the same directory. --- app/meson.build | 4 +-- app/src/command.h | 7 ++++++ app/src/server.c | 51 ++++++++++++++++++++++++++++++++------ app/src/sys/unix/command.c | 26 +++++++++++++++++++ app/src/sys/win/command.c | 15 +++++++++++ meson_options.txt | 2 +- 6 files changed, 94 insertions(+), 11 deletions(-) diff --git a/app/meson.build b/app/meson.build index 673892cd..02d24a34 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,8 +93,8 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# build a "portable" version (with scrcpy-server.jar accessible from the -# current directory) +# build a "portable" version (with scrcpy-server.jar accessible from the same +# directory as the executable) conf.set('PORTABLE', get_option('portable')) # the default client TCP port for the "adb reverse" tunnel diff --git a/app/src/command.h b/app/src/command.h index 3453ca10..db6358da 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -9,6 +9,7 @@ // not needed here, but winsock2.h must never be included AFTER windows.h # include # include +# define PATH_SEPARATOR '\\' # define PRIexitcode "lu" // # ifdef _WIN64 @@ -23,6 +24,7 @@ #else # include +# define PATH_SEPARATOR '/' # define PRIsizet "zu" # define PRIexitcode "d" # define PROCESS_NONE -1 @@ -76,4 +78,9 @@ adb_install(const char *serial, const char *local); bool process_check_success(process_t proc, const char *name); +// return the absolute path of the executable (the scrcpy binary) +// may be NULL on error; to be freed by SDL_free +char * +get_executable_path(void); + #endif diff --git a/app/src/server.c b/app/src/server.c index e7c8712e..d0599bef 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -2,32 +2,67 @@ #include #include +#include #include #include #include #include "config.h" +#include "command.h" #include "log.h" #include "net.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server.jar" -#ifdef PORTABLE -# define DEFAULT_SERVER_PATH SERVER_FILENAME -#else -# define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#endif - +#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME static const char * get_server_path(void) { - const char *server_path = getenv("SCRCPY_SERVER_PATH"); + const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); + if (server_path_env) { + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); + // if the envvar is set, use it + return server_path_env; + } + +#ifndef PORTABLE + LOGD("Using server: " DEFAULT_SERVER_PATH); + // the absolute path is hardcoded + return DEFAULT_SERVER_PATH; +#else + // use scrcpy-server.jar in the same directory as the executable + char *executable_path = get_executable_path(); + if (!executable_path) { + LOGE("Cannot get executable path, " + "using " SERVER_FILENAME " from current directory"); + // not found, use current directory + return SERVER_FILENAME; + } + char *dir = dirname(executable_path); + size_t dirlen = strlen(dir); + + // sizeof(SERVER_FILENAME) gives statically the size including the null byte + size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); + char *server_path = SDL_malloc(len); if (!server_path) { - server_path = DEFAULT_SERVER_PATH; + LOGE("Cannot alloc server path string, " + "using " SERVER_FILENAME " from current directory"); + SDL_free(executable_path); + return SERVER_FILENAME; } + + memcpy(server_path, dir, dirlen); + server_path[dirlen] = PATH_SEPARATOR; + memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); + // the final null byte has been copied with SERVER_FILENAME + + SDL_free(executable_path); + + LOGD("Using server (portable): %s", server_path); return server_path; +#endif } static bool diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index fa41571e..55aea5e8 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -1,9 +1,15 @@ +// for portability #define _POSIX_SOURCE // for kill() +#define _BSD_SOURCE // for readlink() + +// modern glibc will complain without this +#define _DEFAULT_SOURCE #include "command.h" #include #include +#include #include #include #include @@ -98,3 +104,23 @@ cmd_simple_wait(pid_t pid, int *exit_code) { } return !code; } + +char * +get_executable_path(void) { +// +#ifdef __linux__ + char buf[PATH_MAX + 1]; // +1 for the null byte + ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); + if (len == -1) { + perror("readlink"); + return NULL; + } + buf[len] = '\0'; + return SDL_strdup(buf); +#else + // in practice, we only need this feature for portable builds, only used on + // Windows, so we don't care implementing it for every platform + // (it's useful to have a working version on Linux for debugging though) + return NULL; +#endif +} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 8434dc98..484ce9f0 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -75,3 +75,18 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) { } return !code; } + +char * +get_executable_path(void) { + HMODULE hModule = GetModuleHandleW(NULL); + if (!hModule) { + return NULL; + } + WCHAR buf[MAX_PATH + 1]; // +1 for the null byte + int len = GetModuleFileNameW(hModule, buf, MAX_PATH); + if (!len) { + return NULL; + } + buf[len] = '\0'; + return utf8_from_wide_char(buf); +} diff --git a/meson_options.txt b/meson_options.txt index 90b4293c..a443ccb2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,6 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the current directory') +option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') From 4ee1391361e7590cd6f88d87b0efc762a30c82c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 15:39:25 +0200 Subject: [PATCH 81/90] Upgrade FFmpeg (4.1.3) for Windows Include the latest version of FFmpeg in Windows releases. --- Makefile.CrossWindows | 17 +++++++++-------- cross_win32.txt | 4 ++-- cross_win64.txt | 4 ++-- prebuilt-deps/Makefile | 24 ++++++++++++------------ 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 960983df..1d12f5fa 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,10 +100,10 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.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)/" @@ -114,10 +114,11 @@ dist-win64: build-server build-win64 build-win64-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.3-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/cross_win32.txt b/cross_win32.txt index f82056ff..0b3fc339 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.9/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index beca2096..0090de90 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.9/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index f3c171ba..25ee2916 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.1-win32-shared.zip \ - e692b18c01745d262c03294b382fd64df68fabe3c66aa4546a3ad3935175cde3 \ - ffmpeg-4.1-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \ + 8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \ + ffmpeg-4.1.3-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1-win32-dev.zip \ - 34bc5e471fb9160609abd6bc271e361050f3ff7376b1b8a0873cca02b38277c8 \ - ffmpeg-4.1-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \ + e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \ + ffmpeg-4.1.3-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1-win64-shared.zip \ - c4908c97436c946509dc365e421159274fa4b1e66dce6fb5b63d82a6294d5357 \ - ffmpeg-4.1-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \ + 0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \ + ffmpeg-4.1.3-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1-win64-dev.zip \ - 761ec79aa3dae66698c9791a2f0bb9da8794246f8356cadc741ddc0eabab0471 \ - ffmpeg-4.1-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \ + 334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \ + ffmpeg-4.1.3-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz \ From e3afb67e7f7ab01d81a46b5be95bcfcf74277de4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 17:46:16 +0200 Subject: [PATCH 82/90] Downgrade SDL to 2.0.8 for Windows Revert "Update SDL (2.0.9) for Windows" Several users experienced freezes with SDL 2.0.9. This reverts commit a5787dccd62fe4ff9dc05d597d3aab06da717edd. See: - - --- Makefile.CrossWindows | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/Makefile | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 1d12f5fa..7955c544 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -107,7 +107,7 @@ dist-win32: build-server build-win32 build-win32-noconsole 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)/" - cp prebuilt-deps/SDL2-2.0.9/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -122,7 +122,7 @@ dist-win64: build-server build-win64 build-win64-noconsole 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)/" - cp prebuilt-deps/SDL2-2.0.9/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ diff --git a/cross_win32.txt b/cross_win32.txt index 0b3fc339..2db35fe0 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.9/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 0090de90..79181653 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.9/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 25ee2916..8044c34f 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64: ffmpeg-4.1.3-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz \ - 0f9f00d0f2a9a95dfb5cce929718210c3f85432cc2e9d4abade4adcb7f6bb39d \ - SDL2-2.0.9 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \ + ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \ + SDL2-2.0.8 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \ From ffe0417228fb78ab45b7ee4e202fc06fc8875bf3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 17:51:22 +0200 Subject: [PATCH 83/90] Update platform-tools (29.0.1) for Windows Include the latest version of adb in Windows releases. --- prebuilt-deps/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 8044c34f..04f8b779 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.8 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \ - db78f726d5dc653706dcd15a462ab1b946c643f598df76906c4c1858411c54df \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \ + 2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \ platform-tools From 5ffdcbb7beea8721350280170365aa8417c38ab8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:16:31 +0200 Subject: [PATCH 84/90] Update DEVELOP.md --- DEVELOP.md | 75 +++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index ba9e130b..dea8137d 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String... args)`][main] method), compiled against the Android framework, and executed as `shell` on the Android device. -[main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100 +[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 To run such a Java application, the classes must be [_dexed_][dex] (typically, to `classes.dex`). If `my.package.MainClass` is the main class, compiled to @@ -65,18 +65,18 @@ They can be called using reflection though. The communication with hidden components is provided by [_wrappers_ classes][wrappers] and [aidl]. [hidden]: https://stackoverflow.com/a/31908373/1987178 -[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers -[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view +[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers +[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view ### Threading -The server uses 2 threads: +The server uses 3 threads: - the **main** thread, encoding and streaming the video to the client; - - the **controller** thread, listening for _control events_ (typically, - keyboard and mouse events) from the client. - - the **receiver** thread (managed by the controller), sending _device events_ + - the **controller** thread, listening for _control messages_ (typically, + keyboard and mouse events) from the client; + - the **receiver** thread (managed by the controller), sending _device messges_ to the clients (currently, it is only used to send the device clipboard content). @@ -92,9 +92,9 @@ The video is encoded using the [`MediaCodec`] API. The codec takes its input from a [surface] associated to the display, and writes the resulting H.264 stream to the provided output stream (the socket connected to the client). -[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html -[surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70 +[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69 On device [rotation], the codec, surface and display are reinitialized, and a new video stream is produced. @@ -108,31 +108,30 @@ because it avoids to send unnecessary frames, but there are drawbacks: Both problems are [solved][repeat] by the flag [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. -[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 -[repeat]: -https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 +[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 +[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER ### Input events injection -_Control events_ are received from the client by the [`EventController`] (run in -a separate thread). There are several types of input events: +_Control messages_ are received from the client by the [`Controller`] (run in a +separate thread). There are several types of input events: - keycode (cf [`KeyEvent`]), - text (special characters may not be handled by keycodes directly), - mouse motion/click, - mouse scroll, - other commands (e.g. to switch the screen on or to copy the clipboard). -All of them may need to inject input events to the system. To do so, they use -the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our +Some of them need to inject input events to the system. To do so, they use the +_hidden_ method [`InputManager.injectInputEvent`] (exposed by our [`InputManager` wrapper][inject-wrapper]). -[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66 +[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81 [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 -[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 +[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 @@ -188,12 +187,13 @@ The client uses 4 threads: - the **main** thread, executing the SDL event loop, - the **stream** thread, receiving the video and used for decoding and recording, - - the **controller** thread, sending _control events_ to the server. + - the **controller** thread, sending _control messages_ to the server, - the **receiver** thread (managed by the controller), receiving _device - events_ from the client. + messages_ from the client. In addition, another thread can be started if necessary to handle APK -installation or file push requests (via drag&drop on the main window). +installation or file push requests (via drag&drop on the main window) or to +print the framerate regularly in the console. @@ -217,10 +217,10 @@ to decode a new frame while the main thread renders the last one. If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw H.264 packet to the output video file. -[stream]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/stream.h -[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/decoder.h -[video_buffer]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/video_buffer.h -[recorder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/recorder.h +[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h +[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h +[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h +[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h ``` +----------+ +----------+ @@ -234,19 +234,19 @@ H.264 packet to the output video file. ### Controller -The [controller] is responsible to send _control events_ to the device. It runs -in a separate thread, to avoid I/O on the main thread. +The [controller] is responsible to send _control messages_ to the device. It +runs in a separate thread, to avoid I/O on the main thread. On SDL event, received on the main thread, the [input manager][inputmanager] -creates appropriate [_control events_][controlevent]. It is responsible to +creates appropriate [_control messages_][controlmsg]. It is responsible to convert SDL events to Android events (using [convert]). It pushes the _control -events_ to a queue hold by the controller. On its own thread, the controller -takes events from the queue, that it serializes and sends to the client. +messages_ to a queue hold by the controller. On its own thread, the controller +takes messages from the queue, that it serializes and sends to the client. -[controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h -[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h -[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h -[convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h +[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h +[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h +[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h +[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h ### UI and event loop @@ -257,10 +257,9 @@ thread. Events are handled in the [event loop], which either updates the [screen] or delegates to the [input manager][inputmanager]. -[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c -[event loop]: -https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c#L187 -[screen]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/screen.h +[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c +[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201 +[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h ## Hack From 4abe163233f9bfc636ec8387b8a2d286812482ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:20:06 +0200 Subject: [PATCH 85/90] Remove obsolete explanation in FAQ Issue 9 was about stdout/stderr not printed in Windows console. This is solved since the Windows version is cross-compiled from Linux. --- FAQ.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/FAQ.md b/FAQ.md index 76f572ce..4b04d228 100644 --- a/FAQ.md +++ b/FAQ.md @@ -19,10 +19,6 @@ Windows may need some [drivers] to detect your device. [drivers]: https://developer.android.com/studio/run/oem-usb.html -If you still encounter problems, please see [issue 9]. - -[issue 9]: https://github.com/Genymobile/scrcpy/issues/9 - ### Mouse clicks do not work From 02f189b1de51359150d0c7da0ecc5e815c7a3e15 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:37:09 +0200 Subject: [PATCH 86/90] Remove obsolete detail in README Now that scrcpy-server.jar is found in the same directory as the scrcpy executable, using SCRCPY_SERVER_PATH is not particularly useful on Windows anymore --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10f6138e..5026ebe2 100644 --- a/README.md +++ b/README.md @@ -331,8 +331,8 @@ To use a specific _adb_ binary, configure its path in the environment variable ADB=/path/to/adb scrcpy -To override the path of the `scrcpy-server.jar` file (it can be [useful] on -Windows), configure its path in `SCRCPY_SERVER_PATH`. +To override the path of the `scrcpy-server.jar` file, configure its path in +`SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 From 273cec8a92ecc47ec7f336c07a2fa124726901e9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:47:31 +0200 Subject: [PATCH 87/90] Fix typo in test name --- server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index a2683945..01700689 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -10,7 +10,7 @@ public class StringUtilsTest { @Test @SuppressWarnings("checkstyle:MagicNumber") - public void testUtf8Trucate() { + public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); Assert.assertEquals(7, utf8.length); From 1afe9ce2ee806772e837de124a72e83429b83a8d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:48:58 +0200 Subject: [PATCH 88/90] Fix deprecation warning in Java unit test --- .../src/test/java/com/genymobile/scrcpy/StringUtilsTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index 01700689..7d89ee64 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy; -import junit.framework.Assert; - +import org.junit.Assert; import org.junit.Test; import java.nio.charset.StandardCharsets; From 90859f1dcfd9cac1b6ad8c24c3c021242d034ae0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:49:14 +0200 Subject: [PATCH 89/90] Upgrade tarketSdkVersion to 29 This fixes a lint warning. --- server/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 65d1e558..3b262e9a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 + compileSdkVersion 29 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 27 + targetSdkVersion 29 versionCode 9 versionName "1.8" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" From 259d3aee934e60ab7bd28ed97c830052a442c2c8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 21:50:29 +0200 Subject: [PATCH 90/90] Bump version to 1.9 --- meson.build | 2 +- server/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index eb7ee380..053d8c94 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.8', + version: '1.9', meson_version: '>= 0.37', default_options: 'c_std=c11') diff --git a/server/build.gradle b/server/build.gradle index 3b262e9a..d5c1fb00 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 9 - versionName "1.8" + versionCode 10 + versionName "1.9" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes {