diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4ca1829..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: | @@ -202,7 +202,8 @@ jobs: - name: Install dependencies run: | - brew install meson nasm libiconv zlib automake autoconf libtool + brew install meson ninja nasm libiconv zlib automake autoconf \ + libtool - name: Build env: @@ -229,7 +230,7 @@ jobs: path: release/work/build-macos-aarch64/dist-tar/ build-macos-x86_64: - runs-on: macos-15-intel + runs-on: macos-13 steps: - name: Check architecture run: | @@ -244,7 +245,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: brew install meson nasm libiconv zlib automake + run: brew install meson ninja nasm libiconv zlib automake # autoconf and libtool are already installed on macos-13 - name: Build diff --git a/FAQ.md b/FAQ.md index f9da5d1f..5f089cd7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -141,13 +141,12 @@ On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in: (or if only unrelated USB devices are detected), there might be drivers issues. -Please read [#3654], in particular [this comment][#3654-comment1], [the next -one][#3654-comment2] and [this one][#3654-comment3]. +Please read [#3654], in particular [this comment][#3654-comment1] and [the next +one][#3654-comment2]. [#3654]: https://github.com/Genymobile/scrcpy/issues/3654 [#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232 [#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011 -[#3654-comment3]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-2613219725 ## Control issues @@ -167,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/LICENSE b/LICENSE index 3325e9f3..1196b3da 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2026 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 2ee76736..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.4) +# 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,10 +207,10 @@ work][donate]: [donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## License +## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2026 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 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 e47edf88..17b5641d 100755 --- a/app/deps/adb_linux.sh +++ b/app/deps/adb_linux.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init "$@" +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common -VERSION=36.0.0 -URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip" -SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 - -PROJECT_DIR="platform-tools-$VERSION-linux" -FILENAME="$PROJECT_DIR.zip" +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-linux.zip +PROJECT_DIR=platform-tools-$VERSION-linux +SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir -p "$PROJECT_DIR" cd "$PROJECT_DIR" ZIP_PREFIX=platform-tools diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh index a56117dc..8a25915e 100755 --- a/app/deps/adb_macos.sh +++ b/app/deps/adb_macos.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init "$@" +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common -VERSION=36.0.0 -URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-darwin.zip" -SHA256SUM=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd - -PROJECT_DIR="platform-tools-$VERSION-darwin" -FILENAME="$PROJECT_DIR.zip" +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-darwin.zip +PROJECT_DIR=platform-tools-$VERSION-darwin +SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir -p "$PROJECT_DIR" cd "$PROJECT_DIR" ZIP_PREFIX=platform-tools diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh index 69287b1d..d36706b0 100755 --- a/app/deps/adb_windows.sh +++ b/app/deps/adb_windows.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init "$@" +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common -VERSION=36.0.0 -URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-win.zip" -SHA256SUM=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c - -PROJECT_DIR="platform-tools-$VERSION-windows" -FILENAME="$PROJECT_DIR.zip" +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-win.zip +PROJECT_DIR=platform-tools-$VERSION-windows +SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir -p "$PROJECT_DIR" cd "$PROJECT_DIR" ZIP_PREFIX=platform-tools diff --git a/app/deps/_init b/app/deps/common similarity index 92% rename from app/deps/_init rename to app/deps/common index 4612d9fe..daaa96c0 100644 --- a/app/deps/_init +++ b/app/deps/common @@ -1,9 +1,10 @@ +#!/usr/bin/env bash # This file is intended to be sourced by other scripts, not executed process_args() { if [[ $# != 3 ]] then - # : linux, macos, win32 or win64 + # : win32 or win64 # : native or cross # : static or shared echo "Syntax: $0 " >&2 @@ -11,8 +12,8 @@ process_args() { fi HOST="$1" - BUILD_TYPE="$2" - LINK_TYPE="$3" + BUILD_TYPE="$2" # native or cross + LINK_TYPE="$3" # static or shared DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE" if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh index 46fa3954..3069b6fe 100755 --- a/app/deps/dav1d.sh +++ b/app/deps/dav1d.sh @@ -1,22 +1,22 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common process_args "$@" VERSION=1.5.0 -URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz" +FILENAME=dav1d-$VERSION.tar.gz +PROJECT_DIR=dav1d-$VERSION SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b -PROJECT_DIR="dav1d-$VERSION" -FILENAME="$PROJECT_DIR.tar.gz" - cd "$SOURCES_DIR" if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" + get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" fi diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index d6cfad54..fb8b9a25 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -1,22 +1,22 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common process_args "$@" VERSION=7.1.1 -URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz" +FILENAME=ffmpeg-$VERSION.tar.xz +PROJECT_DIR=ffmpeg-$VERSION SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 -PROJECT_DIR="ffmpeg-$VERSION" -FILENAME="$PROJECT_DIR.tar.xz" - cd "$SOURCES_DIR" if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" + get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" fi diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 72170b9e..4be03eb1 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common process_args "$@" -VERSION=1.0.29 -URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" -SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 - -PROJECT_DIR="libusb-$VERSION" -FILENAME="$PROJECT_DIR.tar.gz" +VERSION=1.0.28 +FILENAME=libusb-$VERSION.tar.gz +PROJECT_DIR=libusb-$VERSION +SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe cd "$SOURCES_DIR" @@ -16,7 +16,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" + get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" fi 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 f74ccf41..c3edee58 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash set -ex -. $(dirname ${BASH_SOURCE[0]})/_init +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common process_args "$@" -VERSION=2.32.8 -URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" -SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c - -PROJECT_DIR="sdl-$VERSION" -FILENAME="$PROJECT_DIR.tar.gz" +VERSION=2.32.2 +FILENAME=SDL-$VERSION.tar.gz +PROJECT_DIR=SDL-release-$VERSION +SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 cd "$SOURCES_DIR" @@ -16,9 +16,9 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "$URL" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION" - mv "SDL-release-$VERSION" "$PROJECT_DIR" + 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" @@ -29,7 +29,7 @@ export CXXFLAGS="$CFLAGS" if [[ -d "$DIRNAME" ]] then - echo "'$PWD/$DIRNAME' already exists, not reconfigured" + echo "'$PWD/$HDIRNAME' already exists, not reconfigured" cd "$DIRNAME" else mkdir "$DIRNAME" diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 07df992f..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.4" + VALUE "ProductVersion", "3.2" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d6940449..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". @@ -852,7 +848,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2026 Romain Vimont +Copyright \(co 2018\-2025 Romain Vimont Licensed under the Apache License, Version 2.0. diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 52ed4592..40e9e968 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -103,14 +103,14 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) { static void show_adb_installation_msg(void) { -#ifndef _WIN32 +#ifndef __WINDOWS__ static const struct { const char *binary; const char *command; } 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"}, @@ -331,7 +331,7 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, bool sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote, unsigned flags) { -#ifdef _WIN32 +#ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) local = sc_str_quote(local); @@ -351,7 +351,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, sc_pid pid = sc_adb_execute(argv, flags); -#ifdef _WIN32 +#ifdef __WINDOWS__ free((void *) remote); free((void *) local); #endif @@ -362,7 +362,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags) { -#ifdef _WIN32 +#ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) local = sc_str_quote(local); @@ -377,7 +377,7 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, sc_pid pid = sc_adb_execute(argv, flags); -#ifdef _WIN32 +#ifdef __WINDOWS__ free((void *) local); #endif 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/device_msg.c b/app/src/device_msg.c index 2172d59b..7621c040 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -53,7 +53,7 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, } uint16_t id = sc_read16be(&buf[1]); size_t size = sc_read16be(&buf[3]); - if (size > len - 5) { + if (size < len - 5) { return 0; // not available } uint8_t *data = malloc(size); diff --git a/app/src/display.c b/app/src/display.c index 15f9a1f1..aee8ef80 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -170,7 +170,6 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) { } } - av_frame_unref(display->pending.frame); int r = av_frame_ref(display->pending.frame, frame); if (r) { LOGE("Could not ref frame: %d", r); @@ -182,11 +181,6 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) { return true; } -// Forward declaration -static bool -sc_display_update_texture_internal(struct sc_display *display, - const AVFrame *frame); - static bool sc_display_apply_pending(struct sc_display *display) { if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) { @@ -202,8 +196,7 @@ sc_display_apply_pending(struct sc_display *display) { if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) { assert(display->pending.frame); - bool ok = sc_display_update_texture_internal(display, - display->pending.frame); + bool ok = sc_display_update_texture(display, display->pending.frame); if (!ok) { return false; } diff --git a/app/src/display.h b/app/src/display.h index 49110994..4de9b0a9 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -22,7 +22,7 @@ struct sc_display { struct sc_opengl gl; #ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE - SDL_GLContext gl_context; + SDL_GLContext *gl_context; #endif bool mipmaps; 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/main.c b/app/src/main.c index 968b1934..c58e0be7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,7 +1,6 @@ #include "common.h" #include -#include #ifdef HAVE_V4L2 # include #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9c..b3ff9b36 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -93,7 +93,7 @@ struct scrcpy { #ifdef _WIN32 static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { - if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT) { + if (ctrl_type == CTRL_C_EVENT) { sc_push_event(SDL_QUIT); return TRUE; } @@ -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/screen.c b/app/src/screen.c index da17df0e..1d694f12 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -225,7 +225,7 @@ sc_screen_render_novideo(struct sc_screen *screen) { (void) res; // any error already logged } -#if defined(__APPLE__) || defined(_WIN32) +#if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif @@ -409,7 +409,7 @@ sc_screen_init(struct sc_screen *screen, } else { // without video, the icon is used as window content, it must be present LOGE("Could not load icon"); - goto error_destroy_window; + goto error_destroy_fps_counter; } SDL_Surface *icon_novideo = params->video ? NULL : icon; 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/src/util/net.c b/app/src/util/net.c index 9e9e3ae4..9562ff6b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -2,7 +2,6 @@ #include #include -#include #ifdef _WIN32 # include diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index 6f34a4ef..e31724e2 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -191,8 +191,7 @@ sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size, size_t right_len = MIN(size, oldcap - oldorigin); assert(right_len); - memcpy(newptr, (char *) ptr + (oldorigin * item_size), - right_len * item_size); + memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size); if (size > right_len) { memcpy((char *) newptr + (right_len * item_size), ptr, diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index e43c7ce4..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))); @@ -411,26 +411,6 @@ static void test_serialize_open_hard_keyboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_start_app(void) { - struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_START_APP, - .start_app = { - .name = "firefox", - }, - }; - - uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; - size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 9); - - const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_START_APP, - 7, // length - 'f', 'i', 'r', 'e', 'f', 'o', 'x', // app name - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - static void test_serialize_reset_video(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_RESET_VIDEO, @@ -468,7 +448,6 @@ int main(int argc, char *argv[]) { test_serialize_uhid_input(); test_serialize_uhid_destroy(); test_serialize_open_hard_keyboard(); - test_serialize_start_app(); test_serialize_reset_video(); return 0; } diff --git a/build.gradle b/build.gradle index 8c671e6f..81c91d37 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.android.tools.build:gradle:8.7.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/doc/build.md b/doc/build.md index e1873f93..afe8b21b 100644 --- a/doc/build.md +++ b/doc/build.md @@ -154,11 +154,7 @@ install it manually and make it available from the `PATH`: export PATH="$JAVA_HOME/bin:$PATH" ``` -When following the rest of the build instructions below, make sure you use the -MinGW terminal within MSYS2. - - -### macOS +### Mac OS Install the packages with [Homebrew]: @@ -176,7 +172,8 @@ Additionally, if you want to build the server, install Java 17 from Caskroom, an make it available from the `PATH`: ```bash -brew install openjdk@17 +brew tap homebrew/cask-versions +brew install adoptopenjdk/openjdk/adoptopenjdk17 export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export PATH="$JAVA_HOME/bin:$PATH" ``` @@ -236,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.3.4`][direct-scrcpy-server] - SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e` + - [`scrcpy-server-v3.2`][direct-scrcpy-server] + SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4 +[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: @@ -274,7 +271,7 @@ This installs several files: - `/usr/local/bin/scrcpy` (main app) - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) - `/usr/local/share/man/man1/scrcpy.1` (manpage) - - `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon) + - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon) - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) diff --git a/doc/develop.md b/doc/develop.md index 61102bf4..21949ea6 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -409,8 +409,8 @@ with any client which uses the same protocol. For simplicity, some [server-specific options] have been added to produce raw streams easily: - - `send_device_meta=false`: disable the device metadata (in practice, the - device name) sent on the _first_ socket + - `send_device_meta=false`: disable the device metata (in practice, the device + name) sent on the _first_ socket - `send_frame_meta=false`: disable the 12-byte header for each packet - `send_dummy_byte`: disable the dummy byte sent on forward connections - `send_codec_meta`: disable the codec information (and initial device size for diff --git a/doc/linux.md b/doc/linux.md index fd9eca8c..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.4.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5` + - [`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.4/scrcpy-linux-x86_64-v3.3.4.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 5435e613..b0335d18 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,14 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd` - - [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929` + - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b` + + - [`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.4/scrcpy-macos-aarch64-v3.3.4.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-x86_64-v3.3.4.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/v4l2.md b/doc/v4l2.md index b50eb9b8..54272b2b 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -24,7 +24,7 @@ to create several devices or devices with specific IDs). If you encounter problems detecting your device with Chrome/WebRTC, you can try `exclusive_caps` mode: -```bash +``` sudo modprobe v4l2loopback exclusive_caps=1 ``` @@ -38,13 +38,6 @@ v4l2-ctl --list-devices ls /dev/video* ``` -If a loopback device was not created automatically, you can make a new one: - -```bash -# requires v4l2loopback-utils package -sudo v4l2loopback-ctl add -``` - To start `scrcpy` using a v4l2 sink: ```bash diff --git a/doc/windows.md b/doc/windows.md index cdc30bdc..fb3e3887 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit) - SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38` - - [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit) - SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72` + - [`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.4/scrcpy-win64-v3.3.4.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fbd6c374..b34b7096 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip # https://gradle.org/release-checksums/ -distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/install_release.sh b/install_release.sh index 5930e54e..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.4/scrcpy-server-v3.3.4 -PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e +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 fc82dee1..b64a6c90 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.3.4', + version: '3.2', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index abbbb60a..02508001 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,15 +1,15 @@ apply plugin: 'com.android.application' android { - namespace = 'com.genymobile.scrcpy' - compileSdk 36 + namespace 'com.genymobile.scrcpy' + compileSdk 35 defaultConfig { - applicationId = "com.genymobile.scrcpy" + applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 36 - versionCode 30304 - versionName "3.3.4" - testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" + targetSdkVersion 35 + versionCode 30200 + versionName "3.2" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -18,11 +18,8 @@ android { } } buildFeatures { - buildConfig = true - aidl = true - } - lint { - disable 'UseRequiresApi' + buildConfig true + aidl true } } diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 435ac8f6..8bb8632b 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,10 +12,10 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.3.4 +SCRCPY_VERSION_NAME=3.2 -PLATFORM=${ANDROID_PLATFORM:-36} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0} +PLATFORM=${ANDROID_PLATFORM:-35} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" @@ -86,7 +86,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ echo "Dexing..." cd "$CLASSES_DIR" -if [[ "${PLATFORM%%.*}" -lt 31 ]] +if [[ $PLATFORM -lt 31 ]] then # use dx "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ diff --git a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl index 0d1f1979..2b331175 100644 --- a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl +++ b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl @@ -48,4 +48,19 @@ oneway interface IDisplayWindowListener { * Called when a display is removed from the hierarchy. */ void onDisplayRemoved(int displayId); + + /** + * Called when fixed rotation is started on a display. + */ + void onFixedRotationStarted(int displayId, int newRotation); + + /** + * Called when the previous fixed rotation on a display is finished. + */ + void onFixedRotationFinished(int displayId); + + /** + * Called when the keep clear ares on a display have changed. + */ + void onKeepClearAreasChanged(int displayId, in List restricted, in List unrestricted); } diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index a5816c32..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,10 +188,6 @@ public final class CleanUp { } unlinkSelf(); - // Needed for workarounds - prepareMainLooper(); - Workarounds.apply(); - 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 5d41a8f3..22fc6d49 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,7 +2,6 @@ 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.ContentResolver; @@ -12,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"; @@ -75,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(); } @@ -90,38 +87,8 @@ public final class FakeContext extends ContextWrapper { return this; } - @Override - public Context createPackageContext(String packageName, int flags) { - return this; - } - @Override public ContentResolver getContentResolver() { return contentResolver; } - - @SuppressLint("SoonBlockedPrivateApi") - @Override - public Object getSystemService(String name) { - Object service = super.getSystemService(name); - if (service == null) { - return null; - } - - // "semclipboard" is a Samsung-internal service - // See: - // - - // - - if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name) || Context.ACTIVITY_SERVICE.equals(name)) { - try { - Field field = service.getClass().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 04e4a837..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 { @@ -225,16 +217,10 @@ public final class Server { } private static void internalMain(String... args) throws Exception { - Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Ln.e("Exception on thread " + t, e); - if (defaultHandler != null) { - defaultHandler.uncaughtException(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 1e012de6..fb4c1389 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -6,9 +6,9 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Application; -import android.app.Instrumentation; import android.content.AttributionSource; import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.media.AudioAttributes; import android.media.AudioManager; @@ -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(); @@ -103,7 +118,10 @@ public final class Workarounds { private static void fillAppContext() { try { - Application app = Instrumentation.newApplication(Application.class, FakeContext.get()); + Application app = new Application(); + Field baseField = ContextWrapper.class.getDeclaredField("mBase"); + baseField.setAccessible(true); + baseField.set(app, FakeContext.get()); // activityThread.mInitialApplication = app; Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); 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/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java index 06bcd7eb..86bd1859 100644 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy.opengl; import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Threads; import android.graphics.SurfaceTexture; import android.opengl.EGL14; @@ -16,7 +15,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.view.Surface; -import java.util.concurrent.Callable; import java.util.concurrent.Semaphore; public final class OpenGLRunner { @@ -82,17 +80,31 @@ public final class OpenGLRunner { public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { initOnce(); + // Simulate CompletableFuture, but working for all Android versions + final Semaphore sem = new Semaphore(0); + Throwable[] throwableRef = new Throwable[1]; + // The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly. // See + handler.post(() -> { + try { + run(inputSize, outputSize, outputSurface); + } catch (Throwable throwable) { + throwableRef[0] = throwable; + } finally { + sem.release(); + } + }); + try { - Threads.executeSynchronouslyOn(handler, new Callable() { - @Override - public Void call() throws Exception { - run(inputSize, outputSize, outputSurface); - return null; - } - }); - } catch (Throwable throwable) { + sem.acquire(); + } catch (InterruptedException e) { + // Behave as if this method call was synchronous + Thread.currentThread().interrupt(); + } + + Throwable throwable = throwableRef[0]; + if (throwable != null) { if (throwable instanceof OpenGLException) { throw (OpenGLException) throwable; } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java index 811a4f31..c0700125 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java @@ -74,11 +74,9 @@ public final class Ln { public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { Log.w(TAG, message, throwable); - synchronized (CONSOLE_ERR) { - CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); - if (throwable != null) { - throwable.printStackTrace(CONSOLE_ERR); - } + CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); + if (throwable != null) { + throwable.printStackTrace(CONSOLE_ERR); } } } @@ -90,11 +88,9 @@ public final class Ln { public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - synchronized (CONSOLE_ERR) { - CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); - if (throwable != null) { - throwable.printStackTrace(CONSOLE_ERR); - } + CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); + if (throwable != null) { + throwable.printStackTrace(CONSOLE_ERR); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java index 4eb67d18..e6465525 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,8 +1,13 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.os.Build; + +import java.io.IOException; + public final class Settings { public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; @@ -13,26 +18,66 @@ public final class Settings { /* not instantiable */ } - public static String getValue(String table, String key) throws SettingsException { - try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { - return provider.getValue(table, key); + private static void execSettingsPut(String table, String key, String value) throws SettingsException { + try { + Command.exec("settings", "put", table, key, value); + } catch (IOException | InterruptedException e) { + throw new SettingsException("put", table, key, value, e); } } + private static String execSettingsGet(String table, String key) throws SettingsException { + try { + return Command.execReadLine("settings", "get", table, key); + } catch (IOException | InterruptedException e) { + throw new SettingsException("get", table, key, null, e); + } + } + + public static String getValue(String table, String key) throws SettingsException { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); + } catch (SettingsException e) { + Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); + } + } + + return execSettingsGet(table, key); + } + public static void putValue(String table, String key, String value) throws SettingsException { - try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { - provider.putValue(table, key, value); + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); + } catch (SettingsException e) { + Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); + } } + execSettingsPut(table, key, value); } public static String getAndPutValue(String table, String key, String value) throws SettingsException { - try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { - String oldValue = provider.getValue(table, key); - if (!value.equals(oldValue)) { - provider.putValue(table, key, value); + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); + } + return oldValue; + } catch (SettingsException e) { + Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e); } - return oldValue; } + + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Threads.java b/server/src/main/java/com/genymobile/scrcpy/util/Threads.java deleted file mode 100644 index c561d1ef..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/util/Threads.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.genymobile.scrcpy.util; - -import android.os.Handler; - -import java.util.concurrent.Callable; -import java.util.concurrent.Semaphore; - -public final class Threads { - private Threads() { - // not instantiable - } - - public static T executeSynchronouslyOn(Handler handler, Callable callable) throws Throwable { - // Simulate CompletableFuture, but working for all Android versions - final Semaphore sem = new Semaphore(0); - @SuppressWarnings("unchecked") - T[] resultRef = (T[]) new Object[1]; - Throwable[] throwableRef = new Throwable[1]; - - handler.post(() -> { - try { - resultRef[0] = callable.call(); - } catch (Throwable throwable) { - throwableRef[0] = throwable; - } finally { - sem.release(); - } - }); - - try { - sem.acquire(); - } catch (InterruptedException e) { - // Behave as if this method call was synchronous - Thread.currentThread().interrupt(); - } - - if (throwableRef[0] != null) { - throw throwableRef[0]; - } - - return resultRef[0]; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index e933f60e..792b3a8a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -25,7 +25,6 @@ public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; - private static final int VIRTUAL_DISPLAY_FLAG_PRESENTATION = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; @@ -170,7 +169,6 @@ public class NewDisplayCapture extends SurfaceCapture { int virtualDisplayId; try { int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC - | VIRTUAL_DISPLAY_FLAG_PRESENTATION | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; 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 9e84ec00..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,14 +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; - try { - uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); - } catch (NoSuchFieldException e) { - // This field might not exist: - uniqueId = null; - } - 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/DisplayWindowListener.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java index 1573d817..f2ecb158 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java @@ -1,12 +1,11 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.util.Ln; - import android.content.res.Configuration; -import android.os.Parcel; -import android.os.RemoteException; +import android.graphics.Rect; import android.view.IDisplayWindowListener; +import java.util.List; + public class DisplayWindowListener extends IDisplayWindowListener.Stub { @Override public void onDisplayAdded(int displayId) { @@ -24,14 +23,17 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub { } @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - try { - return super.onTransact(code, data, reply, flags); - } catch (AbstractMethodError e) { - Ln.v("Ignoring AbstractMethodError: " + e.getMessage()); - // Ignore unknown methods, write default response to reply parcel - reply.writeNoException(); - return true; - } + public void onFixedRotationStarted(int displayId, int newRotation) { + // empty default implementation + } + + @Override + public void onFixedRotationFinished(int displayId) { + // empty default implementation + } + + @Override + public void onKeepClearAreasChanged(int displayId, List restricted, List unrestricted) { + // empty default implementation } } 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