diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49402a6e..5875c6bf 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-22.04 + runs-on: ubuntu-20.04 steps: - name: Check architecture run: | diff --git a/FAQ.md b/FAQ.md index 24722c74..5f089cd7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -166,13 +166,14 @@ 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. A trick allows -to also inject some [accented characters][accented-characters], +The default text injection method is [limited to ASCII characters][text-input]. +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 d886d23c..a3b0d834 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.3.1) +# scrcpy (v3.2) scrcpy @@ -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: ``` -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 @@ -207,7 +207,7 @@ work][donate]: [donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## License +## Licence 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 a49da8ca..9918918c 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -205,7 +205,6 @@ _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 04ffb8f1..450fc8f5 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -1,4 +1,4 @@ -#compdef scrcpy scrcpy.exe +#compdef -N scrcpy -N 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 a3e339ec..17b5641d 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=36.0.0 +VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-linux.zip PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 +SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a cd "$SOURCES_DIR" diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh index 36f5df89..8a25915e 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=36.0.0 +VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-darwin.zip PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd +SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 cd "$SOURCES_DIR" diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh index de37162c..d36706b0 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=36.0.0 +VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-win.zip PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 +SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 cd "$SOURCES_DIR" diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 887a2a77..4be03eb1 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=1.0.29 +VERSION=1.0.28 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 +SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe 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 new file mode 100644 index 00000000..cbb516ec --- /dev/null +++ b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch @@ -0,0 +1,33 @@ +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 54fee12b..c3edee58 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.32.8 +VERSION=2.32.2 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c +SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 cd "$SOURCES_DIR" @@ -18,6 +18,7 @@ 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 9c5374ae..19475e0b 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.3.1" + VALUE "ProductVersion", "3.2" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d72fda13..d481ddd1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -510,10 +510,6 @@ 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 9e9cfd6b..40e9e968 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 install --cask android-platform-tools"}, + {"brew", "brew cask install 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 296d1a9f..1995d384 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -75,14 +75,6 @@ # 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 e46c6165..e78f0c57 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -127,14 +127,10 @@ 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); - // 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); + int16_t hscroll = + sc_float_to_i16fp(msg->inject_scroll_event.hscroll); + int16_t vscroll = + sc_float_to_i16fp(msg->inject_scroll_event.vscroll); 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 33f0807e..29cfc594 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, 1 byte for hozizontal scrolling -#define SC_HID_MOUSE_INPUT_SIZE 5 +// 1 byte for wheel motion +#define SC_HID_MOUSE_INPUT_SIZE 4 /** * 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) 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, @@ -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[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); - data[3] = 0; // no vertical scrolling - data[4] = 0; // no horizontal scrolling + data[3] = 0; // wheel coordinates only used for scrolling } 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[1] = 0; // no x motion data[2] = 0; // no y motion - data[3] = 0; // no vertical scrolling - data[4] = 0; // no horizontal scrolling + data[3] = 0; // wheel coordinates only used for scrolling } -bool +void 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 - data[3] = CLAMP(event->vscroll_int, -127, 127); - data[4] = CLAMP(event->hscroll_int, -127, 127); - return true; + // 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 } 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 4ae4bfd4..06c61dd1 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); -bool +void 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 1e34b50e..0c022acc 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -393,8 +393,6 @@ 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 3e4dd0f3..635825c9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -897,14 +897,12 @@ 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 = event->preciseX, - .vscroll = event->preciseY, + .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), + .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), #else - .hscroll = event->x, - .vscroll = event->y, + .hscroll = CLAMP(event->x, -1, 1), + .vscroll = CLAMP(event->y, -1, 1), #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 a4c8c340..b3ff9b36 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -107,17 +107,6 @@ 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"); @@ -176,7 +165,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) { } static enum scrcpy_exit_code -event_loop(struct scrcpy *s, bool has_screen) { +event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { @@ -208,7 +197,7 @@ event_loop(struct scrcpy *s, bool has_screen) { break; } default: - if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { + if (!sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; } break; @@ -944,7 +933,7 @@ aoa_complete: } } - ret = event_loop(s, options->window); + ret = event_loop(s); terminate_event_loop(); LOGD("quit..."); diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 869e48a4..7fed8383 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -55,9 +55,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_uhid *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { - return; - } + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); 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 fd5fa5e0..b64e9b12 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -42,9 +42,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_aoa *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { - return; - } + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); 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 5c580df9..02edc3a3 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -164,15 +164,8 @@ 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 0d19919e..af97182d 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 = 16, - .vscroll = -16, + .hscroll = 1, + .vscroll = -1, .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, // 16 (float encoded as i16 in the range [-16, 16]) - 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) + 0x7F, 0xFF, // 1 (float encoded as i16) + 0x80, 0x00, // -1 (float encoded as i16) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/doc/build.md b/doc/build.md index 7f76b4fd..afe8b21b 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.3.1`][direct-scrcpy-server] - SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8` + - [`scrcpy-server-v3.2`][direct-scrcpy-server] + SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` -[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 configuration: diff --git a/doc/linux.md b/doc/linux.md index be433df4..52345d1a 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.3.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422` + - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be` [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. @@ -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`~~ _(obsolete version)_ + - Snap: `snap install scrcpy` - … (see [repology](https://repology.org/project/scrcpy/versions)) diff --git a/doc/macos.md b/doc/macos.md index f6b01c30..b0335d18 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.3.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139` + - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b` - - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d` + - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e` [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-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-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.2/scrcpy-macos-x86_64-v3.2.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 8fa1921f..fb3e3887 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`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` + - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit) + SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0` + - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit) + SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357` [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-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-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.2/scrcpy-win32-v3.2.zip and extract it. diff --git a/install_release.sh b/install_release.sh index d960932b..2d2d2c2f 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.3.1/scrcpy-server-v3.3.1 -PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 +PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server diff --git a/meson.build b/meson.build index d991d672..b64a6c90 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.3.1', + version: '3.2', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 31092b12..02508001 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30301 - versionName "3.3.1" + versionCode 30200 + versionName "3.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 193a9902..8bb8632b 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.3.1 +SCRCPY_VERSION_NAME=3.2 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 77018afa..51db985c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -7,7 +7,6 @@ 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; @@ -180,11 +179,6 @@ 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 @@ -194,9 +188,6 @@ 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 b43e9e1b..22fc6d49 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,10 +2,8 @@ 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; @@ -13,8 +11,6 @@ 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"; @@ -76,7 +72,7 @@ public final class FakeContext extends ContextWrapper { @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName(PACKAGE_NAME); + builder.setPackageName("shell"); return builder.build(); } @@ -95,25 +91,4 @@ 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 a08c948c..09cfd6cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -24,13 +24,10 @@ 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; @@ -58,7 +55,17 @@ public final class Server { this.fatalError = true; } 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 { if (cleanUp != null) { 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) { int status = 0; try { @@ -229,8 +221,6 @@ 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 b89f19ae..fb4c1389 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -29,6 +29,8 @@ 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"); @@ -75,6 +77,19 @@ 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 830a7ec7..e503ec61 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -112,9 +112,8 @@ public class ControlMessageReader { private ControlMessage parseInjectScrollEvent() throws IOException { 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()) * 16; - float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); 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 b4a8e3ca..5e64a4c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -6,7 +6,6 @@ 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; @@ -18,6 +17,7 @@ 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,20 +114,22 @@ 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(() -> { - 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(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); + } } }); } else { @@ -154,34 +156,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private UhidManager getUhidManager() { if (uhidManager == null) { - 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); + uhidManager = new UhidManager(sender); } - return uhidManager; } @@ -723,9 +699,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (timeout < 0) { return null; } - if (timeout > 0) { - displayDataAvailable.wait(timeout); - } + 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 20532c0b..c4867a3f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -3,7 +3,6 @@ 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; @@ -32,20 +31,14 @@ 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, String displayUniqueId) { + public UhidManager(DeviceMessageSender sender) { this.sender = sender; - this.displayUniqueId = displayUniqueId; if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); @@ -59,22 +52,15 @@ 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); } - String phys = mustUseInputPort() ? INPUT_PORT : null; - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys); + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); Os.write(fd, req, 0, req.length); - if (firstDevice) { - addUniqueIdAssociation(); - } registerUhidListener(id, fd); } catch (Exception e) { 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 { * uint32_t type; @@ -184,23 +170,17 @@ 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[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); - int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); - assert nameLen <= 127; - buf.put(nameBytes, 0, nameLen); + 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); - 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); @@ -239,26 +219,15 @@ 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) { @@ -268,20 +237,4 @@ 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 8d26b7ce..cdd4bab9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -7,18 +7,16 @@ 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, String uniqueId) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; this.dpi = dpi; - this.uniqueId = uniqueId; } public int getDisplayId() { @@ -44,8 +42,5 @@ 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 81168aae..c269750e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java @@ -32,11 +32,9 @@ public enum Orientation { throw new IllegalArgumentException("Unknown orientation: " + name); } - 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 static Orientation fromRotation(int rotation) { + assert rotation >= 0 && rotation < 4; + return values()[rotation]; } 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 54936122..791df0f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,43 +1,270 @@ 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.Context; +import android.content.IOnPrimaryClipChangedListener; +import android.os.Build; +import android.os.IInterface; + +import java.lang.reflect.Method; 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() { - android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); - if (manager == null) { + IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { // Some devices have no clipboard manager // // return null; } - return new ClipboardManager(manager); + return new ClipboardManager(clipboard); } - private ClipboardManager(android.content.ClipboardManager manager) { + private ClipboardManager(IInterface 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() { - ClipData clipData = manager.getPrimaryClip(); - if (clipData == null || clipData.getItemCount() == 0) { + 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); return null; } - return clipData.getItemAt(0).getText(); } public boolean setText(CharSequence text) { - ClipData clipData = ClipData.newPlainText(null, text); - manager.setPrimaryClip(clipData); - return true; + 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; + } } - public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { - manager.addPrimaryClipChangedListener(listener); + 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; + } } } 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 a12470a4..d44ac608 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -46,7 +46,6 @@ 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; @@ -82,7 +81,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, null); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -96,12 +95,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(); @@ -115,18 +114,9 @@ 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 { - Method method = getGetDisplayInfoMethod(); - Object displayInfo = method.invoke(manager, displayId); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); @@ -139,8 +129,7 @@ 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); - String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); } 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 f55648d5..5c5ba56c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,15 +1,11 @@ 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") @@ -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_FINISH = 2; - private final android.hardware.input.InputManager manager; - private long lastPermissionLogDate; + private final Object manager; + private Method injectInputEventMethod; - private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; - private static Method addUniqueIdAssociationByPortMethod; - private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { - android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() - .getSystemService(FakeContext.INPUT_SERVICE); - return new InputManager(manager); + 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); + } } - 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; } - private static Method getInjectInputEventMethod() throws NoSuchMethodException { + private Method getInjectInputEventMethod() throws NoSuchMethodException { 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; } @@ -50,23 +57,6 @@ 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; } @@ -107,40 +97,4 @@ 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 b1123b55..a8a56dab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -54,8 +54,7 @@ public final class ServiceManager { return windowManager; } - // The DisplayManager may be used from both the Controller thread and the video (main) thread - public static synchronized DisplayManager getDisplayManager() { + public static 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 0cc0a6b5..74df064f 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); // -16.0f encoded as i16 (the range is [-16, 16]) + dos.writeShort(0x8000); // -1.0f encoded as i16 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(-16f, event.getVScroll(), 0f); + Assert.assertEquals(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(-1, bis.read()); // EOS