Compare commits

..

No commits in common. "master" and "v3.2" have entirely different histories.

47 changed files with 440 additions and 410 deletions

View file

@ -84,7 +84,7 @@ jobs:
run: release/test_client.sh run: release/test_client.sh
build-linux-x86_64: build-linux-x86_64:
runs-on: ubuntu-22.04 runs-on: ubuntu-20.04
steps: steps:
- name: Check architecture - name: Check architecture
run: | run: |

5
FAQ.md
View file

@ -166,13 +166,14 @@ Rebooting the device is necessary once this option is set.
### Special characters do not work ### Special characters do not work
The default text injection method is limited to ASCII characters. A trick allows The default text injection method is [limited to ASCII characters][text-input].
to also inject some [accented characters][accented-characters], A trick allows to also inject some [accented characters][accented-characters],
but that's all. See [#37]. but that's all. See [#37].
To avoid the problem, [change the keyboard mode to simulate a physical To avoid the problem, [change the keyboard mode to simulate a physical
keyboard][hid]. 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 [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37 [#37]: https://github.com/Genymobile/scrcpy/issues/37
[hid]: doc/keyboard.md#physical-keyboard-simulation [hid]: doc/keyboard.md#physical-keyboard-simulation

View file

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.** their name contains `scrcpy`.**
# scrcpy (v3.3.1) # scrcpy (v3.2)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -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: On some devices (especially Xiaomi), you might get the following error:
``` ```
Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. java.lang.SecurityException: 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 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 [donate]: https://blog.rom1v.com/about/#support-my-open-source-work
## License ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2025 Romain Vimont Copyright (C) 2018-2025 Romain Vimont

View file

@ -205,7 +205,6 @@ _scrcpy() {
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \ |--rotation \
|--screen-off-timeout \
|--tunnel-host \ |--tunnel-host \
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \

View file

@ -1,4 +1,4 @@
#compdef scrcpy scrcpy.exe #compdef -N scrcpy -N scrcpy.exe
# #
# name: scrcpy # name: scrcpy
# auth: hltdev [hltdev8642@gmail.com] # auth: hltdev [hltdev8642@gmail.com]
@ -11,7 +11,7 @@ arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]' '--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--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=[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-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]' '--audio-dup=[Duplicate audio]'
@ -35,10 +35,10 @@ arguments=(
{-e,--select-tcpip}'[Use TCP/IP device]' {-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]' {-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--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)' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
{-h,--help}'[Print the help]' {-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)' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--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-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders 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,--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]' '--max-fps=[Limit the frame rate of screen capture]'
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse-bind=[Configure bindings of secondary clicks]' '--mouse-bind=[Configure bindings of secondary clicks]'

View file

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
VERSION=36.0.0 VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-linux.zip FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux PROJECT_DIR=platform-tools-$VERSION-linux
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
cd "$SOURCES_DIR" cd "$SOURCES_DIR"

View file

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
VERSION=36.0.0 VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-darwin.zip FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin PROJECT_DIR=platform-tools-$VERSION-darwin
SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
cd "$SOURCES_DIR" cd "$SOURCES_DIR"

View file

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
VERSION=36.0.0 VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-win.zip FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
cd "$SOURCES_DIR" cd "$SOURCES_DIR"

View file

@ -5,10 +5,10 @@ cd "$DEPS_DIR"
. common . common
process_args "$@" process_args "$@"
VERSION=1.0.29 VERSION=1.0.28
FILENAME=libusb-$VERSION.tar.gz FILENAME=libusb-$VERSION.tar.gz
PROJECT_DIR=libusb-$VERSION PROJECT_DIR=libusb-$VERSION
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe
cd "$SOURCES_DIR" cd "$SOURCES_DIR"

View file

@ -0,0 +1,33 @@
From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001
From: Neal Gompa <neal@gompa.dev>
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

View file

@ -5,10 +5,10 @@ cd "$DEPS_DIR"
. common . common
process_args "$@" process_args "$@"
VERSION=2.32.8 VERSION=2.32.2
FILENAME=SDL-$VERSION.tar.gz FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -18,6 +18,7 @@ then
else else
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" 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" 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 fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR" mkdir -p "$BUILD_DIR/$PROJECT_DIR"

View file

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe" VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy" VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "3.3.1" VALUE "ProductVersion", "3.2"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View file

@ -510,10 +510,6 @@ The device serial number. Mandatory only if several devices are connected to adb
.B \-S, \-\-turn\-screen\-off .B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately. 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 .TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...] .BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".

View file

@ -110,7 +110,7 @@ show_adb_installation_msg(void) {
} pkg_managers[] = { } pkg_managers[] = {
{"apt", "apt install adb"}, {"apt", "apt install adb"},
{"apt-get", "apt-get install adb"}, {"apt-get", "apt-get install adb"},
{"brew", "brew install --cask android-platform-tools"}, {"brew", "brew cask install android-platform-tools"},
{"dnf", "dnf install android-tools"}, {"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"}, {"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"}, {"pacman", "pacman -S android-tools"},

View file

@ -75,14 +75,6 @@
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL # define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
#endif #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 #ifndef HAVE_STRDUP
char *strdup(const char *s); char *strdup(const char *s);
#endif #endif

View file

@ -127,14 +127,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
return 32; return 32;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position); write_position(&buf[1], &msg->inject_scroll_event.position);
// Accept values in the range [-16, 16]. int16_t hscroll =
// Normalize to [-1, 1] in order to use sc_float_to_i16fp(). sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
float hscroll_norm = msg->inject_scroll_event.hscroll / 16; int16_t vscroll =
hscroll_norm = CLAMP(hscroll_norm, -1, 1); sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
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[13], (uint16_t) hscroll);
sc_write16be(&buf[15], (uint16_t) vscroll); sc_write16be(&buf[15], (uint16_t) vscroll);
sc_write32be(&buf[17], msg->inject_scroll_event.buttons); sc_write32be(&buf[17], msg->inject_scroll_event.buttons);

View file

@ -3,8 +3,8 @@
#include <stdint.h> #include <stdint.h>
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
// 1 byte for wheel motion, 1 byte for hozizontal scrolling // 1 byte for wheel motion
#define SC_HID_MOUSE_INPUT_SIZE 5 #define SC_HID_MOUSE_INPUT_SIZE 4
/** /**
* Mouse descriptor from the specification: * Mouse descriptor from the specification:
@ -75,21 +75,6 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
0x81, 0x06, 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 // End Collection
0xC0, 0xC0,
@ -175,8 +160,7 @@ 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[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = CLAMP(event->xrel, -127, 127); data[1] = CLAMP(event->xrel, -127, 127);
data[2] = CLAMP(event->yrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127);
data[3] = 0; // no vertical scrolling data[3] = 0; // wheel coordinates only used for scrolling
data[4] = 0; // no horizontal scrolling
} }
void void
@ -188,27 +172,22 @@ 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[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = 0; // no x motion data[1] = 0; // no x motion
data[2] = 0; // no y motion data[2] = 0; // no y motion
data[3] = 0; // no vertical scrolling data[3] = 0; // wheel coordinates only used for scrolling
data[4] = 0; // no horizontal scrolling
} }
bool void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event) { 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); sc_hid_mouse_input_init(hid_input);
uint8_t *data = hid_input->data; uint8_t *data = hid_input->data;
data[0] = 0; // buttons state irrelevant (and unknown) data[0] = 0; // buttons state irrelevant (and unknown)
data[1] = 0; // no x motion data[1] = 0; // no x motion
data[2] = 0; // no y motion data[2] = 0; // no y motion
data[3] = CLAMP(event->vscroll_int, -127, 127); // In practice, vscroll is always -1, 0 or 1, but in theory other values
data[4] = CLAMP(event->hscroll_int, -127, 127); // are possible
return true; data[3] = CLAMP(event->vscroll, -127, 127);
// Horizontal scrolling ignored
} }
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {

View file

@ -22,7 +22,7 @@ void
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
const struct sc_mouse_click_event *event); const struct sc_mouse_click_event *event);
bool void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event); const struct sc_mouse_scroll_event *event);

View file

@ -393,8 +393,6 @@ struct sc_mouse_scroll_event {
struct sc_position position; struct sc_position position;
float hscroll; float hscroll;
float vscroll; float vscroll;
int32_t hscroll_int;
int32_t vscroll_int;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
}; };

View file

@ -897,14 +897,12 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
struct sc_mouse_scroll_event evt = { struct sc_mouse_scroll_event evt = {
.position = sc_input_manager_get_position(im, mouse_x, mouse_y), .position = sc_input_manager_get_position(im, mouse_x, mouse_y),
#if SDL_VERSION_ATLEAST(2, 0, 18) #if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = event->preciseX, .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = event->preciseY, .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
#else #else
.hscroll = event->x, .hscroll = CLAMP(event->x, -1, 1),
.vscroll = event->y, .vscroll = CLAMP(event->y, -1, 1),
#endif #endif
.hscroll_int = event->x,
.vscroll_int = event->y,
.buttons_state = im->mouse_buttons_state, .buttons_state = im->mouse_buttons_state,
}; };

View file

@ -107,17 +107,6 @@ sdl_set_hints(const char *render_driver) {
LOGW("Could not set 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 // Linear filtering
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering"); LOGW("Could not enable linear filtering");
@ -176,7 +165,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
} }
static enum scrcpy_exit_code static enum scrcpy_exit_code
event_loop(struct scrcpy *s, bool has_screen) { event_loop(struct scrcpy *s) {
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
switch (event.type) { switch (event.type) {
@ -208,7 +197,7 @@ event_loop(struct scrcpy *s, bool has_screen) {
break; break;
} }
default: default:
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { if (!sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE; return SCRCPY_EXIT_FAILURE;
} }
break; break;
@ -944,7 +933,7 @@ aoa_complete:
} }
} }
ret = event_loop(s, options->window); ret = event_loop(s);
terminate_event_loop(); terminate_event_loop();
LOGD("quit..."); LOGD("quit...");

View file

@ -55,9 +55,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_uhid *mouse = DOWNCAST(mp); struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input; struct sc_hid_input hid_input;
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
return;
}
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
} }

View file

@ -42,9 +42,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_aoa *mouse = DOWNCAST(mp); struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input; struct sc_hid_input hid_input;
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
return;
}
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse scroll)"); LOGW("Could not push AOA HID input (mouse scroll)");

View file

@ -164,15 +164,8 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
struct sc_mouse_scroll_event evt = { struct sc_mouse_scroll_event evt = {
// .position not used for HID events // .position not used for HID events
#if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = event->preciseX,
.vscroll = event->preciseY,
#else
.hscroll = event->x, .hscroll = event->x,
.vscroll = event->y, .vscroll = event->y,
#endif
.hscroll_int = event->x,
.vscroll_int = event->y,
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
}; };

View file

@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) {
.height = 1920, .height = 1920,
}, },
}, },
.hscroll = 16, .hscroll = 1,
.vscroll = -16, .vscroll = -1,
.buttons = 1, .buttons = 1,
}, },
}; };
@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) 0x7F, 0xFF, // 1 (float encoded as i16)
0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x80, 0x00, // -1 (float encoded as i16)
0x00, 0x00, 0x00, 0x01, // 1 0x00, 0x00, 0x00, 0x01, // 1
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));

View file

@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v3.3.1`][direct-scrcpy-server] - [`scrcpy-server-v3.2`][direct-scrcpy-server]
<sub>SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8`</sub> <sub>SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View file

@ -6,11 +6,11 @@
Download a static build of the [latest release]: Download a static build of the [latest release]:
- [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64) - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422`</sub> <sub>SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz [direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz
and extract it. and extract it.
@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers:
- Arch Linux: `pacman -S scrcpy` - Arch Linux: `pacman -S scrcpy`
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
- Gentoo: `emerge scrcpy` - Gentoo: `emerge scrcpy`
- Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions)) - … (see [repology](https://repology.org/project/scrcpy/versions))

View file

@ -6,15 +6,15 @@
Download a static build of the [latest release]: Download a static build of the [latest release]:
- [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64) - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139`</sub> <sub>SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b`</sub>
- [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d`</sub> <sub>SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz [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.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz [direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz
and extract it. and extract it.

View file

@ -6,14 +6,14 @@
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit)
<sub>SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0`</sub> <sub>SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0`</sub>
- [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit)
<sub>SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6`</sub> <sub>SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip [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.3.1/scrcpy-win32-v3.3.1.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip
and extract it. and extract it.

View file

@ -2,8 +2,8 @@
set -e set -e
BUILDDIR=build-auto BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0
echo "[scrcpy] Downloading prebuilt server..." echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View file

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '3.3.1', version: '3.2',
meson_version: '>= 0.49', meson_version: '>= 0.49',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View file

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 35 targetSdkVersion 35
versionCode 30301 versionCode 30200
versionName "3.3.1" versionName "3.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View file

@ -12,7 +12,7 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.3.1 SCRCPY_VERSION_NAME=3.2
PLATFORM=${ANDROID_PLATFORM:-35} PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}

View file

@ -7,7 +7,6 @@ import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Looper;
import android.system.ErrnoException; import android.system.ErrnoException;
import android.system.Os; import android.system.Os;
@ -180,11 +179,6 @@ public final class CleanUp {
} }
} }
@SuppressWarnings("deprecation")
private static void prepareMainLooper() {
Looper.prepareMainLooper();
}
public static void main(String... args) { public static void main(String... args) {
try { try {
// Start a new session to avoid being terminated along with the server process on some devices // Start a new session to avoid being terminated along with the server process on some devices
@ -194,9 +188,6 @@ public final class CleanUp {
} }
unlinkSelf(); unlinkSelf();
// Needed for workarounds
prepareMainLooper();
int displayId = Integer.parseInt(args[0]); int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]); int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean disableShowTouches = Boolean.parseBoolean(args[2]);

View file

@ -2,10 +2,8 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.ClipboardManager;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
@ -13,8 +11,6 @@ import android.content.IContentProvider;
import android.os.Binder; import android.os.Binder;
import android.os.Process; import android.os.Process;
import java.lang.reflect.Field;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell"; public static final String PACKAGE_NAME = "com.android.shell";
@ -76,7 +72,7 @@ public final class FakeContext extends ContextWrapper {
@Override @Override
public AttributionSource getAttributionSource() { public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME); builder.setPackageName("shell");
return builder.build(); return builder.build();
} }
@ -95,25 +91,4 @@ public final class FakeContext extends ContextWrapper {
public ContentResolver getContentResolver() { public ContentResolver getContentResolver() {
return contentResolver; 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;
}
} }

View file

@ -24,13 +24,10 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.annotation.SuppressLint;
import android.os.Build; import android.os.Build;
import android.os.Looper;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -58,7 +55,17 @@ public final class Server {
this.fatalError = true; this.fatalError = true;
} }
if (running == 0 || this.fatalError) { if (running == 0 || this.fatalError) {
Looper.getMainLooper().quitSafely(); notify();
}
}
synchronized void await() {
try {
while (running > 0 && !fatalError) {
wait();
}
} catch (InterruptedException e) {
// ignore
} }
} }
} }
@ -165,7 +172,7 @@ public final class Server {
}); });
} }
Looper.loop(); // interrupted by the Completion implementation completion.await();
} finally { } finally {
if (cleanUp != null) { if (cleanUp != null) {
cleanUp.interrupt(); cleanUp.interrupt();
@ -194,21 +201,6 @@ 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) { public static void main(String... args) {
int status = 0; int status = 0;
try { try {
@ -229,8 +221,6 @@ public final class Server {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
}); });
prepareMainLooper();
Options options = Options.parse(args); Options options = Options.parse(args);
Ln.disableSystemStreams(); Ln.disableSystemStreams();

View file

@ -29,6 +29,8 @@ public final class Workarounds {
private static final Object ACTIVITY_THREAD; private static final Object ACTIVITY_THREAD;
static { static {
prepareMainLooper();
try { try {
// ActivityThread activityThread = new ActivityThread(); // ActivityThread activityThread = new ActivityThread();
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
@ -75,6 +77,19 @@ public final class Workarounds {
fillAppContext(); 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()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// 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"
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();
}
private static void fillAppInfo() { private static void fillAppInfo() {
try { try {
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();

View file

@ -112,9 +112,8 @@ public class ControlMessageReader {
private ControlMessage parseInjectScrollEvent() throws IOException { private ControlMessage parseInjectScrollEvent() throws IOException {
Position position = parsePosition(); Position position = parsePosition();
// Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16]. float hScroll = Binary.i16FixedPointToFloat(dis.readShort());
float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; float vScroll = Binary.i16FixedPointToFloat(dis.readShort());
float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
int buttons = dis.readInt(); int buttons = dis.readInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
} }

View file

@ -6,7 +6,6 @@ import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
@ -18,6 +17,7 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
@ -114,12 +114,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Ln.w("Input events are not supported for secondary displays before Android 10"); 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 (clipboardAutosync) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically // If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager != null) { if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(() -> { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) { if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it // This is a notification for the change we are currently applying, ignore it
return; return;
@ -129,6 +130,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
DeviceMessage msg = DeviceMessage.createClipboard(text); DeviceMessage msg = DeviceMessage.createClipboard(text);
sender.send(msg); sender.send(msg);
} }
}
}); });
} else { } else {
Ln.w("No clipboard manager, copy-paste between device and computer will not work"); Ln.w("No clipboard manager, copy-paste between device and computer will not work");
@ -154,34 +156,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private UhidManager getUhidManager() { private UhidManager getUhidManager() {
if (uhidManager == null) { if (uhidManager == null) {
int uhidDisplayId = displayId; uhidManager = new UhidManager(sender);
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; return uhidManager;
} }
@ -723,9 +699,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (timeout < 0) { if (timeout < 0) {
return null; return null;
} }
if (timeout > 0) {
displayDataAvailable.wait(timeout); displayDataAvailable.wait(timeout);
}
data = displayData.get(); data = displayData.get();
} }

View file

@ -3,7 +3,6 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils; import com.genymobile.scrcpy.util.StringUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build; import android.os.Build;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -32,20 +31,14 @@ public final class UhidManager {
private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) 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<Integer, FileDescriptor> fds = new ArrayMap<>(); private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>();
private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder());
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
private final MessageQueue queue; private final MessageQueue queue;
public UhidManager(DeviceMessageSender sender, String displayUniqueId) { public UhidManager(DeviceMessageSender sender) {
this.sender = sender; this.sender = sender;
this.displayUniqueId = displayUniqueId;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
HandlerThread thread = new HandlerThread("UHidManager"); HandlerThread thread = new HandlerThread("UHidManager");
thread.start(); thread.start();
@ -59,22 +52,15 @@ public final class UhidManager {
try { try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try { try {
// First UHID device added
boolean firstDevice = fds.isEmpty();
FileDescriptor old = fds.put(id, fd); FileDescriptor old = fds.put(id, fd);
if (old != null) { if (old != null) {
Ln.w("Duplicate UHID id: " + id); Ln.w("Duplicate UHID id: " + id);
close(old); close(old);
} }
String phys = mustUseInputPort() ? INPUT_PORT : null; byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc);
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys);
Os.write(fd, req, 0, req.length); Os.write(fd, req, 0, req.length);
if (firstDevice) {
addUniqueIdAssociation();
}
registerUhidListener(id, fd); registerUhidListener(id, fd);
} catch (Exception e) { } catch (Exception e) {
close(fd); close(fd);
@ -162,7 +148,7 @@ public final class UhidManager {
} }
} }
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) {
/* /*
* struct uhid_event { * struct uhid_event {
* uint32_t type; * uint32_t type;
@ -184,23 +170,17 @@ public final class UhidManager {
* } __attribute__((__packed__)); * } __attribute__((__packed__));
*/ */
byte[] empty = new byte[256];
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
buf.putInt(UHID_CREATE2); buf.putInt(UHID_CREATE2);
String actualName = name.isEmpty() ? "scrcpy" : name; String actualName = name.isEmpty() ? "scrcpy" : name;
byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
assert nameLen <= 127; assert len <= 127;
buf.put(nameBytes, 0, nameLen); buf.put(utf8Name, 0, len);
buf.put(empty, 0, 256 - len);
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((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL); buf.putShort(BUS_VIRTUAL);
buf.putInt(vendorId); buf.putInt(vendorId);
@ -239,26 +219,15 @@ public final class UhidManager {
if (fd != null) { if (fd != null) {
unregisterUhidListener(fd); unregisterUhidListener(fd);
close(fd); close(fd);
if (fds.isEmpty()) {
// Last UHID device removed
removeUniqueIdAssociation();
}
} else { } else {
Ln.w("Closing unknown UHID device: " + id); Ln.w("Closing unknown UHID device: " + id);
} }
} }
public void closeAll() { public void closeAll() {
if (fds.isEmpty()) {
return;
}
for (FileDescriptor fd : fds.values()) { for (FileDescriptor fd : fds.values()) {
close(fd); close(fd);
} }
removeUniqueIdAssociation();
} }
private static void close(FileDescriptor fd) { private static void close(FileDescriptor fd) {
@ -268,20 +237,4 @@ public final class UhidManager {
Ln.e("Failed to close uhid: " + e.getMessage()); 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);
}
}
} }

View file

@ -7,18 +7,16 @@ public final class DisplayInfo {
private final int layerStack; private final int layerStack;
private final int flags; private final int flags;
private final int dpi; private final int dpi;
private final String uniqueId;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) {
this.displayId = displayId; this.displayId = displayId;
this.size = size; this.size = size;
this.rotation = rotation; this.rotation = rotation;
this.layerStack = layerStack; this.layerStack = layerStack;
this.flags = flags; this.flags = flags;
this.dpi = dpi; this.dpi = dpi;
this.uniqueId = uniqueId;
} }
public int getDisplayId() { public int getDisplayId() {
@ -44,8 +42,5 @@ public final class DisplayInfo {
public int getDpi() { public int getDpi() {
return dpi; return dpi;
} }
public String getUniqueId() {
return uniqueId;
}
} }

View file

@ -32,11 +32,9 @@ public enum Orientation {
throw new IllegalArgumentException("Unknown orientation: " + name); throw new IllegalArgumentException("Unknown orientation: " + name);
} }
public static Orientation fromRotation(int ccwRotation) { public static Orientation fromRotation(int rotation) {
assert ccwRotation >= 0 && ccwRotation < 4; assert rotation >= 0 && rotation < 4;
// Display rotation is expressed counter-clockwise, orientation is expressed clockwise return values()[rotation];
int cwRotation = (4 - ccwRotation) % 4;
return values()[cwRotation];
} }
public boolean isFlipped() { public boolean isFlipped() {

View file

@ -1,43 +1,270 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.content.ClipData; import android.content.ClipData;
import android.content.Context; import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.Method;
public final class ClipboardManager { public final class ClipboardManager {
private final android.content.ClipboardManager manager; private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
private int getMethodVersion;
private int setMethodVersion;
private int addListenerMethodVersion;
static ClipboardManager create() { static ClipboardManager create() {
android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
if (manager == null) { if (clipboard == null) {
// Some devices have no clipboard manager // Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440> // <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556> // <https://github.com/Genymobile/scrcpy/issues/1556>
return null; return null;
} }
return new ClipboardManager(manager); return new ClipboardManager(clipboard);
} }
private ClipboardManager(android.content.ClipboardManager manager) { private ClipboardManager(IInterface manager) {
this.manager = 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() { public CharSequence getText() {
ClipData clipData = manager.getPrimaryClip(); try {
Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
if (clipData == null || clipData.getItemCount() == 0) { if (clipData == null || clipData.getItemCount() == 0) {
return null; return null;
} }
return clipData.getItemAt(0).getText(); return clipData.getItemAt(0).getText();
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return null;
}
} }
public boolean setText(CharSequence text) { public boolean setText(CharSequence text) {
try {
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
manager.setPrimaryClip(clipData); setPrimaryClip(method, setMethodVersion, manager, clipData);
return true; return true;
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return false;
}
} }
public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
manager.addPrimaryClipChangedListener(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;
}
} }
} }

View file

@ -46,7 +46,6 @@ public final class DisplayManager {
} }
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method getDisplayInfoMethod;
private Method createVirtualDisplayMethod; private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod; private Method requestDisplayPowerMethod;
@ -82,7 +81,7 @@ public final class DisplayManager {
int density = Integer.parseInt(m.group(5)); int density = Integer.parseInt(m.group(5));
int layerStack = Integer.parseInt(m.group(6)); int layerStack = Integer.parseInt(m.group(6));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
} }
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
@ -96,12 +95,12 @@ public final class DisplayManager {
} }
private static int parseDisplayFlags(String text) { private static int parseDisplayFlags(String text) {
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) { if (text == null) {
return 0; return 0;
} }
int flags = 0; int flags = 0;
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
Matcher m = regex.matcher(text); Matcher m = regex.matcher(text);
while (m.find()) { while (m.find()) {
String flagString = m.group(); String flagString = m.group();
@ -115,18 +114,9 @@ public final class DisplayManager {
return flags; 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) { public DisplayInfo getDisplayInfo(int displayId) {
try { try {
Method method = getGetDisplayInfoMethod(); Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
Object displayInfo = method.invoke(manager, displayId);
if (displayInfo == null) { if (displayInfo == null) {
// fallback when displayInfo is null // fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId); return getDisplayInfoFromDumpsysDisplay(displayId);
@ -139,8 +129,7 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View file

@ -1,15 +1,11 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
@ -19,28 +15,39 @@ 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_RESULT = 1;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final android.hardware.input.InputManager manager; private final Object manager;
private long lastPermissionLogDate; private Method injectInputEventMethod;
private static Method injectInputEventMethod;
private static Method setDisplayIdMethod; private static Method setDisplayIdMethod;
private static Method setActionButtonMethod; private static Method setActionButtonMethod;
private static Method addUniqueIdAssociationByPortMethod;
private static Method removeUniqueIdAssociationByPortMethod;
static InputManager create() { static InputManager create() {
android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() try {
.getSystemService(FakeContext.INPUT_SERVICE); Class<?> inputManagerClass = getInputManagerClass();
return new InputManager(manager); Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
return new InputManager(im);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
} }
private InputManager(android.hardware.input.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) {
this.manager = manager; this.manager = manager;
} }
private static Method getInjectInputEventMethod() throws NoSuchMethodException { private Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) { if (injectInputEventMethod == null) {
injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
} }
return injectInputEventMethod; return injectInputEventMethod;
} }
@ -50,23 +57,6 @@ public final class InputManager {
Method method = getInjectInputEventMethod(); Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode); return (boolean) method.invoke(manager, inputEvent, mode);
} catch (ReflectiveOperationException e) { } 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); Ln.e("Could not invoke method", e);
return false; return false;
} }
@ -107,40 +97,4 @@ public final class InputManager {
return false; 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);
}
}
} }

View file

@ -54,8 +54,7 @@ public final class ServiceManager {
return windowManager; return windowManager;
} }
// The DisplayManager may be used from both the Controller thread and the video (main) thread public static DisplayManager getDisplayManager() {
public static synchronized DisplayManager getDisplayManager() {
if (displayManager == null) { if (displayManager == null) {
displayManager = DisplayManager.create(); displayManager = DisplayManager.create();
} }

View file

@ -125,7 +125,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080); dos.writeShort(1080);
dos.writeShort(1920); dos.writeShort(1920);
dos.writeShort(0); // 0.0f encoded as i16 dos.writeShort(0); // 0.0f encoded as i16
dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16]) dos.writeShort(0x8000); // -1.0f encoded as i16
dos.writeInt(1); dos.writeInt(1);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -139,7 +139,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(0f, event.getHScroll(), 0f); Assert.assertEquals(0f, event.getHScroll(), 0f);
Assert.assertEquals(-16f, event.getVScroll(), 0f); Assert.assertEquals(-1f, event.getVScroll(), 0f);
Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(1, event.getButtons());
Assert.assertEquals(-1, bis.read()); // EOS Assert.assertEquals(-1, bis.read()); // EOS