diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5875c6bf..49402a6e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -84,7 +84,7 @@ jobs:
run: release/test_client.sh
build-linux-x86_64:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- name: Check architecture
run: |
diff --git a/FAQ.md b/FAQ.md
index 5f089cd7..24722c74 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -166,14 +166,13 @@ Rebooting the device is necessary once this option is set.
### Special characters do not work
-The default text injection method is [limited to ASCII characters][text-input].
-A trick allows to also inject some [accented characters][accented-characters],
+The default text injection method is limited to ASCII characters. A trick allows
+to also inject some [accented characters][accented-characters],
but that's all. See [#37].
To avoid the problem, [change the keyboard mode to simulate a physical
keyboard][hid].
-[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37
[hid]: doc/keyboard.md#physical-keyboard-simulation
diff --git a/README.md b/README.md
index a3b0d834..d886d23c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
-# scrcpy (v3.2)
+# scrcpy (v3.3.1)
@@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
On some devices (especially Xiaomi), you might get the following error:
```
-java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
+Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
```
In that case, you need to enable [an additional option][control] `USB debugging
@@ -207,7 +207,7 @@ work][donate]:
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work
-## Licence
+## License
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2025 Romain Vimont
diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy
index 9918918c..a49da8ca 100644
--- a/app/data/bash-completion/scrcpy
+++ b/app/data/bash-completion/scrcpy
@@ -205,6 +205,7 @@ _scrcpy() {
|-p|--port \
|--push-target \
|--rotation \
+ |--screen-off-timeout \
|--tunnel-host \
|--tunnel-port \
|--v4l2-buffer \
diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy
index 450fc8f5..04ffb8f1 100644
--- a/app/data/zsh-completion/_scrcpy
+++ b/app/data/zsh-completion/_scrcpy
@@ -1,4 +1,4 @@
-#compdef -N scrcpy -N scrcpy.exe
+#compdef scrcpy scrcpy.exe
#
# name: scrcpy
# auth: hltdev [hltdev8642@gmail.com]
@@ -11,7 +11,7 @@ arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
- '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
+ '--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]'
@@ -35,10 +35,10 @@ arguments=(
{-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
- '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
+ '-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]'
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
{-h,--help}'[Print the help]'
- '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
+ '-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]'
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
@@ -48,7 +48,7 @@ arguments=(
'--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
- '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
+ '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]'
'--max-fps=[Limit the frame rate of screen capture]'
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse-bind=[Configure bindings of secondary clicks]'
diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh
index 17b5641d..a3e339ec 100755
--- a/app/deps/adb_linux.sh
+++ b/app/deps/adb_linux.sh
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
-VERSION=35.0.2
+VERSION=36.0.0
FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux
-SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
+SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8
cd "$SOURCES_DIR"
diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh
index 8a25915e..36f5df89 100755
--- a/app/deps/adb_macos.sh
+++ b/app/deps/adb_macos.sh
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
-VERSION=35.0.2
+VERSION=36.0.0
FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin
-SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
+SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd
cd "$SOURCES_DIR"
diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh
index d36706b0..de37162c 100755
--- a/app/deps/adb_windows.sh
+++ b/app/deps/adb_windows.sh
@@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
-VERSION=35.0.2
+VERSION=36.0.0
FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows
-SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
+SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0
cd "$SOURCES_DIR"
diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh
index 4be03eb1..887a2a77 100755
--- a/app/deps/libusb.sh
+++ b/app/deps/libusb.sh
@@ -5,10 +5,10 @@ cd "$DEPS_DIR"
. common
process_args "$@"
-VERSION=1.0.28
+VERSION=1.0.29
FILENAME=libusb-$VERSION.tar.gz
PROJECT_DIR=libusb-$VERSION
-SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe
+SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74
cd "$SOURCES_DIR"
diff --git a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch
deleted file mode 100644
index cbb516ec..00000000
--- a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch
+++ /dev/null
@@ -1,33 +0,0 @@
-From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001
-From: Neal Gompa
-Date: Mon, 10 Feb 2025 05:00:56 -0500
-Subject: [PATCH] pipewire: Ensure that the correct struct is used for
- enumeration APIs
-
-PipeWire now requires the correct struct type is used, otherwise
-it will fail to compile.
-
-Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f
-
-Fixes: https://github.com/libsdl-org/SDL/issues/12224
-(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6)
----
- src/audio/pipewire/SDL_pipewire.c | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c
-index 889e05decb..5d1bfc28de 100644
---- a/src/audio/pipewire/SDL_pipewire.c
-+++ b/src/audio/pipewire/SDL_pipewire.c
-@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info)
-
- /* Need to parse the parameters to get the sample rate */
- for (i = 0; i < info->n_params; ++i) {
-- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL);
-+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL);
- }
-
- hotplug_core_sync(node);
---
-2.49.0
-
diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh
index c3edee58..54fee12b 100755
--- a/app/deps/sdl.sh
+++ b/app/deps/sdl.sh
@@ -5,10 +5,10 @@ cd "$DEPS_DIR"
. common
process_args "$@"
-VERSION=2.32.2
+VERSION=2.32.8
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
-SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4
+SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c
cd "$SOURCES_DIR"
@@ -18,7 +18,6 @@ then
else
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
- patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc
index 19475e0b..9c5374ae 100644
--- a/app/scrcpy-windows.rc
+++ b/app/scrcpy-windows.rc
@@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy"
- VALUE "ProductVersion", "3.2"
+ VALUE "ProductVersion", "3.3.1"
END
END
BLOCK "VarFileInfo"
diff --git a/app/scrcpy.1 b/app/scrcpy.1
index d481ddd1..d72fda13 100644
--- a/app/scrcpy.1
+++ b/app/scrcpy.1
@@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
+.TP
+.B "\-\-screen\-off\-timeout " seconds
+Set the screen off timeout while scrcpy is running (restore the initial value on exit).
+
.TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c
index 40e9e968..9e9cfd6b 100644
--- a/app/src/adb/adb.c
+++ b/app/src/adb/adb.c
@@ -110,7 +110,7 @@ show_adb_installation_msg(void) {
} pkg_managers[] = {
{"apt", "apt install adb"},
{"apt-get", "apt-get install adb"},
- {"brew", "brew cask install android-platform-tools"},
+ {"brew", "brew install --cask android-platform-tools"},
{"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"},
diff --git a/app/src/compat.h b/app/src/compat.h
index 1995d384..296d1a9f 100644
--- a/app/src/compat.h
+++ b/app/src/compat.h
@@ -75,6 +75,14 @@
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
#endif
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+# define SCRCPY_SDL_HAS_HINT_APP_NAME
+#endif
+
+#if SDL_VERSION_ATLEAST(2, 0, 14)
+# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME
+#endif
+
#ifndef HAVE_STRDUP
char *strdup(const char *s);
#endif
diff --git a/app/src/control_msg.c b/app/src/control_msg.c
index e78f0c57..e46c6165 100644
--- a/app/src/control_msg.c
+++ b/app/src/control_msg.c
@@ -127,10 +127,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
return 32;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
- int16_t hscroll =
- sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
- int16_t vscroll =
- sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
+ // Accept values in the range [-16, 16].
+ // Normalize to [-1, 1] in order to use sc_float_to_i16fp().
+ float hscroll_norm = msg->inject_scroll_event.hscroll / 16;
+ hscroll_norm = CLAMP(hscroll_norm, -1, 1);
+ float vscroll_norm = msg->inject_scroll_event.vscroll / 16;
+ vscroll_norm = CLAMP(vscroll_norm, -1, 1);
+ int16_t hscroll = sc_float_to_i16fp(hscroll_norm);
+ int16_t vscroll = sc_float_to_i16fp(vscroll_norm);
sc_write16be(&buf[13], (uint16_t) hscroll);
sc_write16be(&buf[15], (uint16_t) vscroll);
sc_write32be(&buf[17], msg->inject_scroll_event.buttons);
diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c
index 29cfc594..33f0807e 100644
--- a/app/src/hid/hid_mouse.c
+++ b/app/src/hid/hid_mouse.c
@@ -3,8 +3,8 @@
#include
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
-// 1 byte for wheel motion
-#define SC_HID_MOUSE_INPUT_SIZE 4
+// 1 byte for wheel motion, 1 byte for hozizontal scrolling
+#define SC_HID_MOUSE_INPUT_SIZE 5
/**
* Mouse descriptor from the specification:
@@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
0x81, 0x06,
+ // Usage Page (Consumer Page)
+ 0x05, 0x0C,
+ // Usage(AC Pan)
+ 0x0A, 0x38, 0x02,
+ // Logical Minimum (-127)
+ 0x15, 0x81,
+ // Logical Maximum (127)
+ 0x25, 0x7F,
+ // Report Size (8)
+ 0x75, 0x08,
+ // Report Count (1)
+ 0x95, 0x01,
+ // Input (Data, Variable, Relative): 1 byte (AC Pan)
+ 0x81, 0x06,
+
// End Collection
0xC0,
@@ -160,7 +175,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = CLAMP(event->xrel, -127, 127);
data[2] = CLAMP(event->yrel, -127, 127);
- data[3] = 0; // wheel coordinates only used for scrolling
+ data[3] = 0; // no vertical scrolling
+ data[4] = 0; // no horizontal scrolling
}
void
@@ -172,22 +188,27 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = 0; // no x motion
data[2] = 0; // no y motion
- data[3] = 0; // wheel coordinates only used for scrolling
+ data[3] = 0; // no vertical scrolling
+ data[4] = 0; // no horizontal scrolling
}
-void
+bool
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event) {
+ if (!event->vscroll_int && !event->hscroll_int) {
+ // Need a full integral value for HID
+ return false;
+ }
+
sc_hid_mouse_input_init(hid_input);
uint8_t *data = hid_input->data;
data[0] = 0; // buttons state irrelevant (and unknown)
data[1] = 0; // no x motion
data[2] = 0; // no y motion
- // In practice, vscroll is always -1, 0 or 1, but in theory other values
- // are possible
- data[3] = CLAMP(event->vscroll, -127, 127);
- // Horizontal scrolling ignored
+ data[3] = CLAMP(event->vscroll_int, -127, 127);
+ data[4] = CLAMP(event->hscroll_int, -127, 127);
+ return true;
}
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h
index 06c61dd1..4ae4bfd4 100644
--- a/app/src/hid/hid_mouse.h
+++ b/app/src/hid/hid_mouse.h
@@ -22,7 +22,7 @@ void
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
const struct sc_mouse_click_event *event);
-void
+bool
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event);
diff --git a/app/src/input_events.h b/app/src/input_events.h
index 0c022acc..1e34b50e 100644
--- a/app/src/input_events.h
+++ b/app/src/input_events.h
@@ -393,6 +393,8 @@ struct sc_mouse_scroll_event {
struct sc_position position;
float hscroll;
float vscroll;
+ int32_t hscroll_int;
+ int32_t vscroll_int;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
};
diff --git a/app/src/input_manager.c b/app/src/input_manager.c
index 635825c9..3e4dd0f3 100644
--- a/app/src/input_manager.c
+++ b/app/src/input_manager.c
@@ -897,12 +897,14 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
struct sc_mouse_scroll_event evt = {
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
#if SDL_VERSION_ATLEAST(2, 0, 18)
- .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
- .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
+ .hscroll = event->preciseX,
+ .vscroll = event->preciseY,
#else
- .hscroll = CLAMP(event->x, -1, 1),
- .vscroll = CLAMP(event->y, -1, 1),
+ .hscroll = event->x,
+ .vscroll = event->y,
#endif
+ .hscroll_int = event->x,
+ .vscroll_int = event->y,
.buttons_state = im->mouse_buttons_state,
};
diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c
index b3ff9b36..a4c8c340 100644
--- a/app/src/scrcpy.c
+++ b/app/src/scrcpy.c
@@ -107,6 +107,17 @@ sdl_set_hints(const char *render_driver) {
LOGW("Could not set render driver");
}
+ // App name used in various contexts (such as PulseAudio)
+#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME)
+ if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) {
+ LOGW("Could not set app name");
+ }
+#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME)
+ if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) {
+ LOGW("Could not set audio device app name");
+ }
+#endif
+
// Linear filtering
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering");
@@ -165,7 +176,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
}
static enum scrcpy_exit_code
-event_loop(struct scrcpy *s) {
+event_loop(struct scrcpy *s, bool has_screen) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
@@ -197,7 +208,7 @@ event_loop(struct scrcpy *s) {
break;
}
default:
- if (!sc_screen_handle_event(&s->screen, &event)) {
+ if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
}
break;
@@ -933,7 +944,7 @@ aoa_complete:
}
}
- ret = event_loop(s);
+ ret = event_loop(s, options->window);
terminate_event_loop();
LOGD("quit...");
diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c
index 7fed8383..869e48a4 100644
--- a/app/src/uhid/mouse_uhid.c
+++ b/app/src/uhid/mouse_uhid.c
@@ -55,7 +55,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input;
- sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
+ if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) {
+ return;
+ }
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
}
diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c
index b64e9b12..fd5fa5e0 100644
--- a/app/src/usb/mouse_aoa.c
+++ b/app/src/usb/mouse_aoa.c
@@ -42,7 +42,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input;
- sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
+ if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) {
+ return;
+ }
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse scroll)");
diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c
index 02edc3a3..5c580df9 100644
--- a/app/src/usb/screen_otg.c
+++ b/app/src/usb/screen_otg.c
@@ -164,8 +164,15 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
struct sc_mouse_scroll_event evt = {
// .position not used for HID events
+#if SDL_VERSION_ATLEAST(2, 0, 18)
+ .hscroll = event->preciseX,
+ .vscroll = event->preciseY,
+#else
.hscroll = event->x,
.vscroll = event->y,
+#endif
+ .hscroll_int = event->x,
+ .vscroll_int = event->y,
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c
index af97182d..0d19919e 100644
--- a/app/tests/test_control_msg_serialize.c
+++ b/app/tests/test_control_msg_serialize.c
@@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) {
.height = 1920,
},
},
- .hscroll = 1,
- .vscroll = -1,
+ .hscroll = 16,
+ .vscroll = -16,
.buttons = 1,
},
};
@@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
- 0x7F, 0xFF, // 1 (float encoded as i16)
- 0x80, 0x00, // -1 (float encoded as i16)
+ 0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16])
+ 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16])
0x00, 0x00, 0x00, 0x01, // 1
};
assert(!memcmp(buf, expected, sizeof(expected)));
diff --git a/doc/build.md b/doc/build.md
index afe8b21b..7f76b4fd 100644
--- a/doc/build.md
+++ b/doc/build.md
@@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- - [`scrcpy-server-v3.2`][direct-scrcpy-server]
- SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0`
+ - [`scrcpy-server-v3.3.1`][direct-scrcpy-server]
+ SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8`
-[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
+[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
diff --git a/doc/linux.md b/doc/linux.md
index 52345d1a..be433df4 100644
--- a/doc/linux.md
+++ b/doc/linux.md
@@ -6,11 +6,11 @@
Download a static build of the [latest release]:
- - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64)
- SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be`
+ - [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64)
+ SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422`
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
-[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz
+[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz
and extract it.
@@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers:
- Arch Linux: `pacman -S scrcpy`
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
- Gentoo: `emerge scrcpy`
- - Snap: `snap install scrcpy`
+ - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_
- … (see [repology](https://repology.org/project/scrcpy/versions))
diff --git a/doc/macos.md b/doc/macos.md
index b0335d18..f6b01c30 100644
--- a/doc/macos.md
+++ b/doc/macos.md
@@ -6,15 +6,15 @@
Download a static build of the [latest release]:
- - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64)
- SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b`
+ - [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64)
+ SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139`
- - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64)
- SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e`
+ - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64)
+ SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d`
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
-[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz
-[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz
+[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz
+[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz
and extract it.
diff --git a/doc/windows.md b/doc/windows.md
index fb3e3887..8fa1921f 100644
--- a/doc/windows.md
+++ b/doc/windows.md
@@ -6,14 +6,14 @@
Download the [latest release]:
- - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit)
- SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0`
- - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit)
- SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357`
+ - [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit)
+ SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0`
+ - [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit)
+ SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6`
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
-[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip
-[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip
+[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip
+[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-v3.3.1.zip
and extract it.
diff --git a/install_release.sh b/install_release.sh
index 2d2d2c2f..d960932b 100755
--- a/install_release.sh
+++ b/install_release.sh
@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
-PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
-PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0
+PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1
+PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
diff --git a/meson.build b/meson.build
index b64a6c90..d991d672 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
project('scrcpy', 'c',
- version: '3.2',
+ version: '3.3.1',
meson_version: '>= 0.49',
default_options: [
'c_std=c11',
diff --git a/server/build.gradle b/server/build.gradle
index 02508001..31092b12 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 35
- versionCode 30200
- versionName "3.2"
+ versionCode 30301
+ versionName "3.3.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh
index 8bb8632b..193a9902 100755
--- a/server/build_without_gradle.sh
+++ b/server/build_without_gradle.sh
@@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
-SCRCPY_VERSION_NAME=3.2
+SCRCPY_VERSION_NAME=3.3.1
PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
index 51db985c..77018afa 100644
--- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
+++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java
@@ -7,6 +7,7 @@ import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.BatteryManager;
+import android.os.Looper;
import android.system.ErrnoException;
import android.system.Os;
@@ -179,6 +180,11 @@ public final class CleanUp {
}
}
+ @SuppressWarnings("deprecation")
+ private static void prepareMainLooper() {
+ Looper.prepareMainLooper();
+ }
+
public static void main(String... args) {
try {
// Start a new session to avoid being terminated along with the server process on some devices
@@ -188,6 +194,9 @@ public final class CleanUp {
}
unlinkSelf();
+ // Needed for workarounds
+ prepareMainLooper();
+
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
index 22fc6d49..b43e9e1b 100644
--- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
+++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
@@ -2,8 +2,10 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.AttributionSource;
+import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
@@ -11,6 +13,8 @@ import android.content.IContentProvider;
import android.os.Binder;
import android.os.Process;
+import java.lang.reflect.Field;
+
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
@@ -72,7 +76,7 @@ public final class FakeContext extends ContextWrapper {
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
- builder.setPackageName("shell");
+ builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
@@ -91,4 +95,25 @@ public final class FakeContext extends ContextWrapper {
public ContentResolver getContentResolver() {
return contentResolver;
}
+
+ @SuppressLint("SoonBlockedPrivateApi")
+ @Override
+ public Object getSystemService(String name) {
+ Object service = super.getSystemService(name);
+ if (service == null) {
+ return null;
+ }
+
+ if (Context.CLIPBOARD_SERVICE.equals(name)) {
+ try {
+ Field field = ClipboardManager.class.getDeclaredField("mContext");
+ field.setAccessible(true);
+ field.set(service, this);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return service;
+ }
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java
index 09cfd6cf..a08c948c 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Server.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Server.java
@@ -24,10 +24,13 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
+import android.annotation.SuppressLint;
import android.os.Build;
+import android.os.Looper;
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
@@ -55,17 +58,7 @@ public final class Server {
this.fatalError = true;
}
if (running == 0 || this.fatalError) {
- notify();
- }
- }
-
- synchronized void await() {
- try {
- while (running > 0 && !fatalError) {
- wait();
- }
- } catch (InterruptedException e) {
- // ignore
+ Looper.getMainLooper().quitSafely();
}
}
}
@@ -172,7 +165,7 @@ public final class Server {
});
}
- completion.await();
+ Looper.loop(); // interrupted by the Completion implementation
} finally {
if (cleanUp != null) {
cleanUp.interrupt();
@@ -201,6 +194,21 @@ public final class Server {
}
}
+ private static void prepareMainLooper() {
+ // Like Looper.prepareMainLooper(), but with quitAllowed set to true
+ Looper.prepare();
+ synchronized (Looper.class) {
+ try {
+ @SuppressLint("DiscouragedPrivateApi")
+ Field field = Looper.class.getDeclaredField("sMainLooper");
+ field.setAccessible(true);
+ field.set(null, Looper.myLooper());
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
public static void main(String... args) {
int status = 0;
try {
@@ -221,6 +229,8 @@ public final class Server {
Ln.e("Exception on thread " + t, e);
});
+ prepareMainLooper();
+
Options options = Options.parse(args);
Ln.disableSystemStreams();
diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
index fb4c1389..b89f19ae 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
@@ -29,8 +29,6 @@ public final class Workarounds {
private static final Object ACTIVITY_THREAD;
static {
- prepareMainLooper();
-
try {
// ActivityThread activityThread = new ActivityThread();
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
@@ -77,19 +75,6 @@ public final class Workarounds {
fillAppContext();
}
- @SuppressWarnings("deprecation")
- private static void prepareMainLooper() {
- // Some devices internally create a Handler when creating an input Surface, causing an exception:
- // "Can't create handler inside thread that has not called Looper.prepare()"
- //
- //
- // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
- // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
- // on a null object reference"
- //
- Looper.prepareMainLooper();
- }
-
private static void fillAppInfo() {
try {
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
index e503ec61..830a7ec7 100644
--- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java
@@ -112,8 +112,9 @@ public class ControlMessageReader {
private ControlMessage parseInjectScrollEvent() throws IOException {
Position position = parsePosition();
- float hScroll = Binary.i16FixedPointToFloat(dis.readShort());
- float vScroll = Binary.i16FixedPointToFloat(dis.readShort());
+ // Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16].
+ float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
+ float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
int buttons = dis.readInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
index 5e64a4c5..b4a8e3ca 100644
--- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java
@@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp;
+import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size;
@@ -17,7 +18,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
-import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent;
import android.os.Build;
import android.os.SystemClock;
@@ -114,22 +114,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
+ // Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled)
+ ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardAutosync) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
- ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager != null) {
- clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
- @Override
- public void dispatchPrimaryClipChanged() {
- if (isSettingClipboard.get()) {
- // This is a notification for the change we are currently applying, ignore it
- return;
- }
- String text = Device.getClipboardText();
- if (text != null) {
- DeviceMessage msg = DeviceMessage.createClipboard(text);
- sender.send(msg);
- }
+ clipboardManager.addPrimaryClipChangedListener(() -> {
+ if (isSettingClipboard.get()) {
+ // This is a notification for the change we are currently applying, ignore it
+ return;
+ }
+ String text = Device.getClipboardText();
+ if (text != null) {
+ DeviceMessage msg = DeviceMessage.createClipboard(text);
+ sender.send(msg);
}
});
} else {
@@ -156,8 +154,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private UhidManager getUhidManager() {
if (uhidManager == null) {
- uhidManager = new UhidManager(sender);
+ int uhidDisplayId = displayId;
+ if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
+ if (displayId == Device.DISPLAY_ID_NONE) {
+ // Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be
+ // associated to the virtual display
+ try {
+ // Wait for at most 1 second until a virtual display id is known
+ DisplayData data = waitDisplayData(1000);
+ if (data != null) {
+ uhidDisplayId = data.virtualDisplayId;
+ }
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ }
+
+ String displayUniqueId = null;
+ if (uhidDisplayId > 0) {
+ // Ignore Device.DISPLAY_ID_NONE and 0 (main display)
+ DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId);
+ if (displayInfo != null) {
+ displayUniqueId = displayInfo.getUniqueId();
+ }
+ }
+ uhidManager = new UhidManager(sender, displayUniqueId);
}
+
return uhidManager;
}
@@ -699,7 +723,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (timeout < 0) {
return null;
}
- displayDataAvailable.wait(timeout);
+ if (timeout > 0) {
+ displayDataAvailable.wait(timeout);
+ }
data = displayData.get();
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java
index c4867a3f..20532c0b 100644
--- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java
@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
+import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import android.os.HandlerThread;
@@ -31,14 +32,20 @@ public final class UhidManager {
private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event)
+ // Must be unique across the system
+ private static final String INPUT_PORT = "scrcpy:" + Os.getpid();
+
+ private final String displayUniqueId;
+
private final ArrayMap fds = new ArrayMap<>();
private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder());
private final DeviceMessageSender sender;
private final MessageQueue queue;
- public UhidManager(DeviceMessageSender sender) {
+ public UhidManager(DeviceMessageSender sender, String displayUniqueId) {
this.sender = sender;
+ this.displayUniqueId = displayUniqueId;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
HandlerThread thread = new HandlerThread("UHidManager");
thread.start();
@@ -52,15 +59,22 @@ public final class UhidManager {
try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try {
+ // First UHID device added
+ boolean firstDevice = fds.isEmpty();
+
FileDescriptor old = fds.put(id, fd);
if (old != null) {
Ln.w("Duplicate UHID id: " + id);
close(old);
}
- byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc);
+ String phys = mustUseInputPort() ? INPUT_PORT : null;
+ byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys);
Os.write(fd, req, 0, req.length);
+ if (firstDevice) {
+ addUniqueIdAssociation();
+ }
registerUhidListener(id, fd);
} catch (Exception e) {
close(fd);
@@ -148,7 +162,7 @@ public final class UhidManager {
}
}
- private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) {
+ private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) {
/*
* struct uhid_event {
* uint32_t type;
@@ -170,17 +184,23 @@ public final class UhidManager {
* } __attribute__((__packed__));
*/
- byte[] empty = new byte[256];
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
buf.putInt(UHID_CREATE2);
String actualName = name.isEmpty() ? "scrcpy" : name;
- byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
- int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
- assert len <= 127;
- buf.put(utf8Name, 0, len);
- buf.put(empty, 0, 256 - len);
+ byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8);
+ int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127);
+ assert nameLen <= 127;
+ buf.put(nameBytes, 0, nameLen);
+ if (phys != null) {
+ buf.position(4 + 128);
+ byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII);
+ assert physBytes.length <= 63;
+ buf.put(physBytes);
+ }
+
+ buf.position(4 + 256);
buf.putShort((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL);
buf.putInt(vendorId);
@@ -219,15 +239,26 @@ public final class UhidManager {
if (fd != null) {
unregisterUhidListener(fd);
close(fd);
+
+ if (fds.isEmpty()) {
+ // Last UHID device removed
+ removeUniqueIdAssociation();
+ }
} else {
Ln.w("Closing unknown UHID device: " + id);
}
}
public void closeAll() {
+ if (fds.isEmpty()) {
+ return;
+ }
+
for (FileDescriptor fd : fds.values()) {
close(fd);
}
+
+ removeUniqueIdAssociation();
}
private static void close(FileDescriptor fd) {
@@ -237,4 +268,20 @@ public final class UhidManager {
Ln.e("Failed to close uhid: " + e.getMessage());
}
}
+
+ private boolean mustUseInputPort() {
+ return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null;
+ }
+
+ private void addUniqueIdAssociation() {
+ if (mustUseInputPort()) {
+ ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId);
+ }
+ }
+
+ private void removeUniqueIdAssociation() {
+ if (mustUseInputPort()) {
+ ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT);
+ }
+ }
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java
index cdd4bab9..8d26b7ce 100644
--- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java
@@ -7,16 +7,18 @@ public final class DisplayInfo {
private final int layerStack;
private final int flags;
private final int dpi;
+ private final String uniqueId;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
- public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) {
+ public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) {
this.displayId = displayId;
this.size = size;
this.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
this.dpi = dpi;
+ this.uniqueId = uniqueId;
}
public int getDisplayId() {
@@ -42,5 +44,8 @@ public final class DisplayInfo {
public int getDpi() {
return dpi;
}
-}
+ public String getUniqueId() {
+ return uniqueId;
+ }
+}
diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java
index c269750e..81168aae 100644
--- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java
+++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java
@@ -32,9 +32,11 @@ public enum Orientation {
throw new IllegalArgumentException("Unknown orientation: " + name);
}
- public static Orientation fromRotation(int rotation) {
- assert rotation >= 0 && rotation < 4;
- return values()[rotation];
+ public static Orientation fromRotation(int ccwRotation) {
+ assert ccwRotation >= 0 && ccwRotation < 4;
+ // Display rotation is expressed counter-clockwise, orientation is expressed clockwise
+ int cwRotation = (4 - ccwRotation) % 4;
+ return values()[cwRotation];
}
public boolean isFlipped() {
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 791df0f8..54936122 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java
@@ -1,270 +1,43 @@
package com.genymobile.scrcpy.wrappers;
-import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
-import com.genymobile.scrcpy.util.Ln;
import android.content.ClipData;
-import android.content.IOnPrimaryClipChangedListener;
-import android.os.Build;
-import android.os.IInterface;
-
-import java.lang.reflect.Method;
+import android.content.Context;
public final class ClipboardManager {
- private final IInterface manager;
- private Method getPrimaryClipMethod;
- private Method setPrimaryClipMethod;
- private Method addPrimaryClipChangedListener;
- private int getMethodVersion;
- private int setMethodVersion;
- private int addListenerMethodVersion;
+ private final android.content.ClipboardManager manager;
static ClipboardManager create() {
- IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
- if (clipboard == null) {
+ android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE);
+ if (manager == null) {
// Some devices have no clipboard manager
//
//
return null;
}
- return new ClipboardManager(clipboard);
+ return new ClipboardManager(manager);
}
- private ClipboardManager(IInterface manager) {
+ private ClipboardManager(android.content.ClipboardManager manager) {
this.manager = manager;
}
- private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
- if (getPrimaryClipMethod == null) {
- if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
- getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
- return getPrimaryClipMethod;
- }
-
- try {
- getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
- getMethodVersion = 0;
- return getPrimaryClipMethod;
- } catch (NoSuchMethodException e) {
- // fall-through
- }
-
- try {
- getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
- getMethodVersion = 1;
- return getPrimaryClipMethod;
- } catch (NoSuchMethodException e) {
- // fall-through
- }
-
- try {
- getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
- getMethodVersion = 2;
- return getPrimaryClipMethod;
- } catch (NoSuchMethodException e) {
- // fall-through
- }
-
- try {
- getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
- getMethodVersion = 3;
- return getPrimaryClipMethod;
- } catch (NoSuchMethodException e) {
- // fall-through
- }
-
- try {
- getPrimaryClipMethod = manager.getClass()
- .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
- getMethodVersion = 4;
- return getPrimaryClipMethod;
- } catch (NoSuchMethodException e) {
- // fall-through
- }
-
- try {
- getPrimaryClipMethod = manager.getClass()
- .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class);
- getMethodVersion = 5;
- return getPrimaryClipMethod;
- } catch (NoSuchMethodException e) {
- // fall-through
- }
-
- getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class);
- getMethodVersion = 6;
- }
- return getPrimaryClipMethod;
- }
-
- private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
- if (setPrimaryClipMethod == null) {
- if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
- setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
- return setPrimaryClipMethod;
- }
-
- try {
- setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
- setMethodVersion = 0;
- return setPrimaryClipMethod;
- } catch (NoSuchMethodException e1) {
- // fall-through
- }
-
- try {
- setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
- setMethodVersion = 1;
- return setPrimaryClipMethod;
- } catch (NoSuchMethodException e2) {
- // fall-through
- }
-
- try {
- setPrimaryClipMethod = manager.getClass()
- .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
- setMethodVersion = 2;
- return setPrimaryClipMethod;
- } catch (NoSuchMethodException e3) {
- // fall-through
- }
-
- setPrimaryClipMethod = manager.getClass()
- .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
- setMethodVersion = 3;
- }
- return setPrimaryClipMethod;
- }
-
- private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
- if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
- }
-
- switch (methodVersion) {
- case 0:
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
- case 1:
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
- case 2:
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
- case 3:
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
- case 4:
- // The last boolean parameter is "userOperate"
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
- case 5:
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
- default:
- return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null);
- }
- }
-
- private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
- if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
- method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
- return;
- }
-
- switch (methodVersion) {
- case 0:
- method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
- break;
- case 1:
- method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
- break;
- case 2:
- method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
- break;
- default:
- // The last boolean parameter is "userOperate"
- method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
- }
- }
-
public CharSequence getText() {
- try {
- Method method = getGetPrimaryClipMethod();
- ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
- if (clipData == null || clipData.getItemCount() == 0) {
- return null;
- }
- return clipData.getItemAt(0).getText();
- } catch (ReflectiveOperationException e) {
- Ln.e("Could not invoke method", e);
+ ClipData clipData = manager.getPrimaryClip();
+ if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
+ return clipData.getItemAt(0).getText();
}
public boolean setText(CharSequence text) {
- try {
- Method method = getSetPrimaryClipMethod();
- ClipData clipData = ClipData.newPlainText(null, text);
- setPrimaryClip(method, setMethodVersion, manager, clipData);
- return true;
- } catch (ReflectiveOperationException e) {
- Ln.e("Could not invoke method", e);
- return false;
- }
+ ClipData clipData = ClipData.newPlainText(null, text);
+ manager.setPrimaryClip(clipData);
+ return true;
}
- private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
- throws ReflectiveOperationException {
- if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
- method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
- return;
- }
-
- switch (methodVersion) {
- case 0:
- method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
- break;
- case 1:
- method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
- break;
- default:
- method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
- break;
- }
- }
-
- private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
- if (addPrimaryClipChangedListener == null) {
- if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
- addPrimaryClipChangedListener = manager.getClass()
- .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
- } else {
- try {
- addPrimaryClipChangedListener = manager.getClass()
- .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
- addListenerMethodVersion = 0;
- } catch (NoSuchMethodException e1) {
- try {
- addPrimaryClipChangedListener = manager.getClass()
- .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
- int.class);
- addListenerMethodVersion = 1;
- } catch (NoSuchMethodException e2) {
- addPrimaryClipChangedListener = manager.getClass()
- .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
- int.class, int.class);
- addListenerMethodVersion = 2;
- }
- }
- }
- }
- return addPrimaryClipChangedListener;
- }
-
- public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
- try {
- Method method = getAddPrimaryClipChangedListener();
- addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
- return true;
- } catch (ReflectiveOperationException e) {
- Ln.e("Could not invoke method", e);
- return false;
- }
+ public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) {
+ manager.addPrimaryClipChangedListener(listener);
}
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
index d44ac608..a12470a4 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java
@@ -46,6 +46,7 @@ public final class DisplayManager {
}
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
+ private Method getDisplayInfoMethod;
private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod;
@@ -81,7 +82,7 @@ public final class DisplayManager {
int density = Integer.parseInt(m.group(5));
int layerStack = Integer.parseInt(m.group(6));
- return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
+ return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null);
}
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
@@ -95,12 +96,12 @@ public final class DisplayManager {
}
private static int parseDisplayFlags(String text) {
- Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) {
return 0;
}
int flags = 0;
+ Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
Matcher m = regex.matcher(text);
while (m.find()) {
String flagString = m.group();
@@ -114,9 +115,18 @@ public final class DisplayManager {
return flags;
}
+ // getDisplayInfo() may be used from both the Controller thread and the video (main) thread
+ private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException {
+ if (getDisplayInfoMethod == null) {
+ getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class);
+ }
+ return getDisplayInfoMethod;
+ }
+
public DisplayInfo getDisplayInfo(int displayId) {
try {
- Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
+ Method method = getGetDisplayInfoMethod();
+ Object displayInfo = method.invoke(manager, displayId);
if (displayInfo == null) {
// fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId);
@@ -129,7 +139,8 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
- return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi);
+ String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
+ return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java
index 5c5ba56c..f55648d5 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java
@@ -1,11 +1,15 @@
package com.genymobile.scrcpy.wrappers;
+import com.genymobile.scrcpy.AndroidVersions;
+import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
import android.view.InputEvent;
import android.view.MotionEvent;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
@@ -15,39 +19,28 @@ public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
- private final Object manager;
- private Method injectInputEventMethod;
+ private final android.hardware.input.InputManager manager;
+ private long lastPermissionLogDate;
+ private static Method injectInputEventMethod;
private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
+ private static Method addUniqueIdAssociationByPortMethod;
+ private static Method removeUniqueIdAssociationByPortMethod;
static InputManager create() {
- try {
- Class> inputManagerClass = getInputManagerClass();
- Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
- Object im = getInstanceMethod.invoke(null);
- return new InputManager(im);
- } catch (ReflectiveOperationException e) {
- throw new AssertionError(e);
- }
+ android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get()
+ .getSystemService(FakeContext.INPUT_SERVICE);
+ return new InputManager(manager);
}
- private static Class> getInputManagerClass() {
- try {
- // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
- return Class.forName("android.hardware.input.InputManagerGlobal");
- } catch (ClassNotFoundException e) {
- return android.hardware.input.InputManager.class;
- }
- }
-
- private InputManager(Object manager) {
+ private InputManager(android.hardware.input.InputManager manager) {
this.manager = manager;
}
- private Method getInjectInputEventMethod() throws NoSuchMethodException {
+ private static Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) {
- injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
+ injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class);
}
return injectInputEventMethod;
}
@@ -57,6 +50,23 @@ public final class InputManager {
Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode);
} catch (ReflectiveOperationException e) {
+ if (e instanceof InvocationTargetException) {
+ Throwable cause = e.getCause();
+ if (cause instanceof SecurityException) {
+ String message = e.getCause().getMessage();
+ if (message != null && message.contains("INJECT_EVENTS permission")) {
+ // Do not flood the console, limit to one permission error log every 3 seconds
+ long now = System.currentTimeMillis();
+ if (lastPermissionLogDate <= now - 3000) {
+ Ln.e(message);
+ Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device.");
+ lastPermissionLogDate = now;
+ }
+ // Do not print the stack trace
+ return false;
+ }
+ }
+ }
Ln.e("Could not invoke method", e);
return false;
}
@@ -97,4 +107,40 @@ public final class InputManager {
return false;
}
}
+
+ private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
+ if (addUniqueIdAssociationByPortMethod == null) {
+ addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
+ "addUniqueIdAssociationByPort", String.class, String.class);
+ }
+ return addUniqueIdAssociationByPortMethod;
+ }
+
+ @TargetApi(AndroidVersions.API_35_ANDROID_15)
+ public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) {
+ try {
+ Method method = getAddUniqueIdAssociationByPortMethod();
+ method.invoke(manager, inputPort, uniqueId);
+ } catch (ReflectiveOperationException e) {
+ Ln.e("Cannot add unique id association by port", e);
+ }
+ }
+
+ private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
+ if (removeUniqueIdAssociationByPortMethod == null) {
+ removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
+ "removeUniqueIdAssociationByPort", String.class);
+ }
+ return removeUniqueIdAssociationByPortMethod;
+ }
+
+ @TargetApi(AndroidVersions.API_35_ANDROID_15)
+ public void removeUniqueIdAssociationByPort(String inputPort) {
+ try {
+ Method method = getRemoveUniqueIdAssociationByPortMethod();
+ method.invoke(manager, inputPort);
+ } catch (ReflectiveOperationException e) {
+ Ln.e("Cannot remove unique id association by port", 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 a8a56dab..b1123b55 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java
@@ -54,7 +54,8 @@ public final class ServiceManager {
return windowManager;
}
- public static DisplayManager getDisplayManager() {
+ // The DisplayManager may be used from both the Controller thread and the video (main) thread
+ public static synchronized DisplayManager getDisplayManager() {
if (displayManager == null) {
displayManager = DisplayManager.create();
}
diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
index 74df064f..0cc0a6b5 100644
--- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java
@@ -125,7 +125,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0); // 0.0f encoded as i16
- dos.writeShort(0x8000); // -1.0f encoded as i16
+ dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16])
dos.writeInt(1);
byte[] packet = bos.toByteArray();
@@ -139,7 +139,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(0f, event.getHScroll(), 0f);
- Assert.assertEquals(-1f, event.getVScroll(), 0f);
+ Assert.assertEquals(-16f, event.getVScroll(), 0f);
Assert.assertEquals(1, event.getButtons());
Assert.assertEquals(-1, bis.read()); // EOS