diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5875c6bf..c4ca1829 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check architecture run: | @@ -202,8 +202,7 @@ jobs: - name: Install dependencies run: | - brew install meson ninja nasm libiconv zlib automake autoconf \ - libtool + brew install meson nasm libiconv zlib automake autoconf libtool - name: Build env: @@ -230,7 +229,7 @@ jobs: path: release/work/build-macos-aarch64/dist-tar/ build-macos-x86_64: - runs-on: macos-13 + runs-on: macos-15-intel steps: - name: Check architecture run: | @@ -245,7 +244,7 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies - run: brew install meson ninja nasm libiconv zlib automake + run: brew install meson nasm libiconv zlib automake # autoconf and libtool are already installed on macos-13 - name: Build diff --git a/FAQ.md b/FAQ.md index 5f089cd7..f9da5d1f 100644 --- a/FAQ.md +++ b/FAQ.md @@ -141,12 +141,13 @@ 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] and [the next -one][#3654-comment2]. +Please read [#3654], in particular [this comment][#3654-comment1], [the next +one][#3654-comment2] and [this one][#3654-comment3]. [#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 @@ -166,14 +167,13 @@ Rebooting the device is necessary once this option is set. ### Special characters do not work -The default text injection method is [limited to ASCII characters][text-input]. -A trick allows to also inject some [accented characters][accented-characters], +The default text injection method is limited to ASCII characters. A trick allows +to also inject some [accented characters][accented-characters], but that's all. See [#37]. To avoid the problem, [change the keyboard mode to simulate a physical keyboard][hid]. -[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [#37]: https://github.com/Genymobile/scrcpy/issues/37 [hid]: doc/keyboard.md#physical-keyboard-simulation diff --git a/LICENSE b/LICENSE index 1196b3da..3325e9f3 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2025 Romain Vimont + Copyright (C) 2018-2026 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 a3b0d834..2ee76736 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.2) +# scrcpy (v3.3.4) 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: ``` -java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. +Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. ``` In that case, you need to enable [an additional option][control] `USB debugging @@ -207,10 +207,10 @@ work][donate]: [donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## Licence +## License Copyright (C) 2018 Genymobile - Copyright (C) 2018-2025 Romain Vimont + Copyright (C) 2018-2026 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 9918918c..a49da8ca 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -205,6 +205,7 @@ _scrcpy() { |-p|--port \ |--push-target \ |--rotation \ + |--screen-off-timeout \ |--tunnel-host \ |--tunnel-port \ |--v4l2-buffer \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 450fc8f5..04ffb8f1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -1,4 +1,4 @@ -#compdef -N scrcpy -N scrcpy.exe +#compdef scrcpy scrcpy.exe # # name: scrcpy # auth: hltdev [hltdev8642@gmail.com] @@ -11,7 +11,7 @@ arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--angle=[Rotate the video content by a custom angle, in degrees]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' - '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' + '--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-dup=[Duplicate audio]' @@ -35,10 +35,10 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]' + '-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' - '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]' + '-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -48,7 +48,7 @@ arguments=( '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' + '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' diff --git a/app/deps/common b/app/deps/_init similarity index 92% rename from app/deps/common rename to app/deps/_init index daaa96c0..4612d9fe 100644 --- a/app/deps/common +++ b/app/deps/_init @@ -1,10 +1,9 @@ -#!/usr/bin/env bash # This file is intended to be sourced by other scripts, not executed process_args() { if [[ $# != 3 ]] then - # : win32 or win64 + # : linux, macos, win32 or win64 # : native or cross # : static or shared echo "Syntax: $0 " >&2 @@ -12,8 +11,8 @@ process_args() { fi HOST="$1" - BUILD_TYPE="$2" # native or cross - LINK_TYPE="$3" # static or shared + BUILD_TYPE="$2" + LINK_TYPE="$3" DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE" if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh index 17b5641d..e47edf88 100755 --- a/app/deps/adb_linux.sh +++ b/app/deps/adb_linux.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init "$@" -VERSION=35.0.2 -FILENAME=platform-tools_r$VERSION-linux.zip -PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a +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" cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + get_file "$URL" "$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 8a25915e..a56117dc 100755 --- a/app/deps/adb_macos.sh +++ b/app/deps/adb_macos.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init "$@" -VERSION=35.0.2 -FILENAME=platform-tools_r$VERSION-darwin.zip -PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 +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" cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + get_file "$URL" "$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 d36706b0..69287b1d 100755 --- a/app/deps/adb_windows.sh +++ b/app/deps/adb_windows.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init "$@" -VERSION=35.0.2 -FILENAME=platform-tools_r$VERSION-win.zip -PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 +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" cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + get_file "$URL" "$FILENAME" "$SHA256SUM" mkdir -p "$PROJECT_DIR" cd "$PROJECT_DIR" ZIP_PREFIX=platform-tools diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh index 3069b6fe..46fa3954 100755 --- a/app/deps/dav1d.sh +++ b/app/deps/dav1d.sh @@ -1,22 +1,22 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init process_args "$@" VERSION=1.5.0 -FILENAME=dav1d-$VERSION.tar.gz -PROJECT_DIR=dav1d-$VERSION +URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz" 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 "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM" + get_file "$URL" "$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 fb8b9a25..d6cfad54 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -1,22 +1,22 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init process_args "$@" VERSION=7.1.1 -FILENAME=ffmpeg-$VERSION.tar.xz -PROJECT_DIR=ffmpeg-$VERSION +URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz" 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 "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" + get_file "$URL" "$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 4be03eb1..72170b9e 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init process_args "$@" -VERSION=1.0.28 -FILENAME=libusb-$VERSION.tar.gz -PROJECT_DIR=libusb-$VERSION -SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe +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" cd "$SOURCES_DIR" @@ -16,7 +16,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" + get_file "$URL" "$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 deleted file mode 100644 index cbb516ec..00000000 --- a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch +++ /dev/null @@ -1,33 +0,0 @@ -From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001 -From: Neal Gompa -Date: Mon, 10 Feb 2025 05:00:56 -0500 -Subject: [PATCH] pipewire: Ensure that the correct struct is used for - enumeration APIs - -PipeWire now requires the correct struct type is used, otherwise -it will fail to compile. - -Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f - -Fixes: https://github.com/libsdl-org/SDL/issues/12224 -(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6) ---- - src/audio/pipewire/SDL_pipewire.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c -index 889e05decb..5d1bfc28de 100644 ---- a/src/audio/pipewire/SDL_pipewire.c -+++ b/src/audio/pipewire/SDL_pipewire.c -@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info) - - /* Need to parse the parameters to get the sample rate */ - for (i = 0; i < info->n_params; ++i) { -- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL); -+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL); - } - - hotplug_core_sync(node); --- -2.49.0 - diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c3edee58..f74ccf41 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash set -ex -DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DEPS_DIR" -. common +. $(dirname ${BASH_SOURCE[0]})/_init process_args "$@" -VERSION=2.32.2 -FILENAME=SDL-$VERSION.tar.gz -PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 +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" cd "$SOURCES_DIR" @@ -16,9 +16,9 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" - tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" - patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch + get_file "$URL" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION" + mv "SDL-release-$VERSION" "$PROJECT_DIR" fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" @@ -29,7 +29,7 @@ export CXXFLAGS="$CFLAGS" if [[ -d "$DIRNAME" ]] then - echo "'$PWD/$HDIRNAME' already exists, not reconfigured" + echo "'$PWD/$DIRNAME' already exists, not reconfigured" cd "$DIRNAME" else mkdir "$DIRNAME" diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 19475e0b..07df992f 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.2" + VALUE "ProductVersion", "3.3.4" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d481ddd1..d6940449 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. +.TP +.B "\-\-screen\-off\-timeout " seconds +Set the screen off timeout while scrcpy is running (restore the initial value on exit). + .TP .BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". @@ -848,7 +852,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2025 Romain Vimont +Copyright \(co 2018\-2026 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 40e9e968..52ed4592 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 __WINDOWS__ +#ifndef _WIN32 static const struct { const char *binary; const char *command; } pkg_managers[] = { {"apt", "apt install adb"}, {"apt-get", "apt-get install adb"}, - {"brew", "brew cask install android-platform-tools"}, + {"brew", "brew install --cask android-platform-tools"}, {"dnf", "dnf install android-tools"}, {"emerge", "emerge dev-util/android-tools"}, {"pacman", "pacman -S android-tools"}, @@ -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 __WINDOWS__ +#ifdef _WIN32 // 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 __WINDOWS__ +#ifdef _WIN32 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 __WINDOWS__ +#ifdef _WIN32 // 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 __WINDOWS__ +#ifdef _WIN32 free((void *) local); #endif diff --git a/app/src/compat.h b/app/src/compat.h index 1995d384..296d1a9f 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -75,6 +75,14 @@ # define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL #endif +#if SDL_VERSION_ATLEAST(2, 0, 18) +# define SCRCPY_SDL_HAS_HINT_APP_NAME +#endif + +#if SDL_VERSION_ATLEAST(2, 0, 14) +# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME +#endif + #ifndef HAVE_STRDUP char *strdup(const char *s); #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e78f0c57..e46c6165 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -127,10 +127,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 32; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - int16_t hscroll = - sc_float_to_i16fp(msg->inject_scroll_event.hscroll); - int16_t vscroll = - sc_float_to_i16fp(msg->inject_scroll_event.vscroll); + // Accept values in the range [-16, 16]. + // Normalize to [-1, 1] in order to use sc_float_to_i16fp(). + float hscroll_norm = msg->inject_scroll_event.hscroll / 16; + hscroll_norm = CLAMP(hscroll_norm, -1, 1); + float vscroll_norm = msg->inject_scroll_event.vscroll / 16; + vscroll_norm = CLAMP(vscroll_norm, -1, 1); + int16_t hscroll = sc_float_to_i16fp(hscroll_norm); + int16_t vscroll = sc_float_to_i16fp(vscroll_norm); sc_write16be(&buf[13], (uint16_t) hscroll); sc_write16be(&buf[15], (uint16_t) vscroll); sc_write32be(&buf[17], msg->inject_scroll_event.buttons); diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 7621c040..2172d59b 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 aee8ef80..15f9a1f1 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -170,6 +170,7 @@ 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); @@ -181,6 +182,11 @@ 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) { @@ -196,7 +202,8 @@ 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(display, display->pending.frame); + bool ok = sc_display_update_texture_internal(display, + display->pending.frame); if (!ok) { return false; } diff --git a/app/src/display.h b/app/src/display.h index 4de9b0a9..49110994 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 29cfc594..33f0807e 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -3,8 +3,8 @@ #include // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, -// 1 byte for wheel motion -#define SC_HID_MOUSE_INPUT_SIZE 4 +// 1 byte for wheel motion, 1 byte for hozizontal scrolling +#define SC_HID_MOUSE_INPUT_SIZE 5 /** * Mouse descriptor from the specification: @@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) 0x81, 0x06, + // Usage Page (Consumer Page) + 0x05, 0x0C, + // Usage(AC Pan) + 0x0A, 0x38, 0x02, + // Logical Minimum (-127) + 0x15, 0x81, + // Logical Maximum (127) + 0x25, 0x7F, + // Report Size (8) + 0x75, 0x08, + // Report Count (1) + 0x95, 0x01, + // Input (Data, Variable, Relative): 1 byte (AC Pan) + 0x81, 0x06, + // End Collection 0xC0, @@ -160,7 +175,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); - data[3] = 0; // wheel coordinates only used for scrolling + data[3] = 0; // no vertical scrolling + data[4] = 0; // no horizontal scrolling } void @@ -172,22 +188,27 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion - data[3] = 0; // wheel coordinates only used for scrolling + data[3] = 0; // no vertical scrolling + data[4] = 0; // no horizontal scrolling } -void +bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event) { + if (!event->vscroll_int && !event->hscroll_int) { + // Need a full integral value for HID + return false; + } + sc_hid_mouse_input_init(hid_input); uint8_t *data = hid_input->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion - // In practice, vscroll is always -1, 0 or 1, but in theory other values - // are possible - data[3] = CLAMP(event->vscroll, -127, 127); - // Horizontal scrolling ignored + data[3] = CLAMP(event->vscroll_int, -127, 127); + data[4] = CLAMP(event->hscroll_int, -127, 127); + return true; } void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 06c61dd1..4ae4bfd4 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -22,7 +22,7 @@ void sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, const struct sc_mouse_click_event *event); -void +bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event); diff --git a/app/src/input_events.h b/app/src/input_events.h index 0c022acc..1e34b50e 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -393,6 +393,8 @@ struct sc_mouse_scroll_event { struct sc_position position; float hscroll; float vscroll; + int32_t hscroll_int; + int32_t vscroll_int; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 635825c9..3e4dd0f3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -897,12 +897,14 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, struct sc_mouse_scroll_event evt = { .position = sc_input_manager_get_position(im, mouse_x, mouse_y), #if SDL_VERSION_ATLEAST(2, 0, 18) - .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), - .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), + .hscroll = event->preciseX, + .vscroll = event->preciseY, #else - .hscroll = CLAMP(event->x, -1, 1), - .vscroll = CLAMP(event->y, -1, 1), + .hscroll = event->x, + .vscroll = event->y, #endif + .hscroll_int = event->x, + .vscroll_int = event->y, .buttons_state = im->mouse_buttons_state, }; diff --git a/app/src/main.c b/app/src/main.c index c58e0be7..968b1934 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,6 +1,7 @@ #include "common.h" #include +#include #ifdef HAVE_V4L2 # include #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b3ff9b36..aedfdf9c 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) { + if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT) { sc_push_event(SDL_QUIT); return TRUE; } @@ -107,6 +107,17 @@ sdl_set_hints(const char *render_driver) { LOGW("Could not set render driver"); } + // App name used in various contexts (such as PulseAudio) +#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME) + if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) { + LOGW("Could not set app name"); + } +#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME) + if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) { + LOGW("Could not set audio device app name"); + } +#endif + // Linear filtering if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); @@ -165,7 +176,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) { } static enum scrcpy_exit_code -event_loop(struct scrcpy *s) { +event_loop(struct scrcpy *s, bool has_screen) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { @@ -197,7 +208,7 @@ event_loop(struct scrcpy *s) { break; } default: - if (!sc_screen_handle_event(&s->screen, &event)) { + if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; } break; @@ -933,7 +944,7 @@ aoa_complete: } } - ret = event_loop(s); + ret = event_loop(s, options->window); terminate_event_loop(); LOGD("quit..."); diff --git a/app/src/screen.c b/app/src/screen.c index 1d694f12..da17df0e 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(__WINDOWS__) +#if defined(__APPLE__) || defined(_WIN32) # 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_fps_counter; + goto error_destroy_window; } 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 7fed8383..869e48a4 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -55,7 +55,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_uhid *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_scroll(&hid_input, event); + if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { + return; + } sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index b64e9b12..fd5fa5e0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -42,7 +42,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_aoa *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_scroll(&hid_input, event); + if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { + return; + } if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { LOGW("Could not push AOA HID input (mouse scroll)"); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 02edc3a3..5c580df9 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -164,8 +164,15 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, struct sc_mouse_scroll_event evt = { // .position not used for HID events +#if SDL_VERSION_ATLEAST(2, 0, 18) + .hscroll = event->preciseX, + .vscroll = event->preciseY, +#else .hscroll = event->x, .vscroll = event->y, +#endif + .hscroll_int = event->x, + .vscroll_int = event->y, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; diff --git a/app/src/util/net.c b/app/src/util/net.c index 9562ff6b..9e9e3ae4 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -2,6 +2,7 @@ #include #include +#include #ifdef _WIN32 # include diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index e31724e2..6f34a4ef 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -191,7 +191,8 @@ 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 af97182d..e43c7ce4 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) { .height = 1920, }, }, - .hscroll = 1, - .vscroll = -1, + .hscroll = 16, + .vscroll = -16, .buttons = 1, }, }; @@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x7F, 0xFF, // 1 (float encoded as i16) - 0x80, 0x00, // -1 (float encoded as i16) + 0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) + 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -411,6 +411,26 @@ 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, @@ -448,6 +468,7 @@ 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 81c91d37..8c671e6f 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.7.1' + classpath 'com.android.tools.build:gradle:8.13.0' // 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 afe8b21b..e1873f93 100644 --- a/doc/build.md +++ b/doc/build.md @@ -154,7 +154,11 @@ install it manually and make it available from the `PATH`: export PATH="$JAVA_HOME/bin:$PATH" ``` -### Mac OS +When following the rest of the build instructions below, make sure you use the +MinGW terminal within MSYS2. + + +### macOS Install the packages with [Homebrew]: @@ -172,8 +176,7 @@ Additionally, if you want to build the server, install Java 17 from Caskroom, an make it available from the `PATH`: ```bash -brew tap homebrew/cask-versions -brew install adoptopenjdk/openjdk/adoptopenjdk17 +brew install openjdk@17 export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export PATH="$JAVA_HOME/bin:$PATH" ``` @@ -233,10 +236,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.2`][direct-scrcpy-server] - SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` + - [`scrcpy-server-v3.3.4`][direct-scrcpy-server] + SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4 Download the prebuilt server somewhere, and specify its path during the Meson configuration: @@ -271,7 +274,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/icon.png` (app icon) + - `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.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 21949ea6..61102bf4 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 metata (in practice, the device - name) sent on the _first_ socket + - `send_device_meta=false`: disable the device metadata (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 52345d1a..fd9eca8c 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be` + - [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz and extract it. @@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers: - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` - - Snap: `snap install scrcpy` + - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - … (see [repology](https://repology.org/project/scrcpy/versions)) diff --git a/doc/macos.md b/doc/macos.md index b0335d18..5435e613 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,14 @@ Download a static build of the [latest release]: - - [`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` + - [`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` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.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 and extract it. diff --git a/doc/v4l2.md b/doc/v4l2.md index 54272b2b..b50eb9b8 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,6 +38,13 @@ 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 fb3e3887..cdc30bdc 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit) - SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0` - - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit) - SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357` + - [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit) + SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38` + - [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit) + SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.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 and extract it. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b34b7096..fbd6c374 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.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip # https://gradle.org/release-checksums/ -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/install_release.sh b/install_release.sh index 2d2d2c2f..5930e54e 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 -PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4 +PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server diff --git a/meson.build b/meson.build index b64a6c90..fc82dee1 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.2', + version: '3.3.4', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 02508001..abbbb60a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,15 +1,15 @@ apply plugin: 'com.android.application' android { - namespace 'com.genymobile.scrcpy' - compileSdk 35 + namespace = 'com.genymobile.scrcpy' + compileSdk 36 defaultConfig { - applicationId "com.genymobile.scrcpy" + applicationId = "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 35 - versionCode 30200 - versionName "3.2" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + targetSdkVersion 36 + versionCode 30304 + versionName "3.3.4" + testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -18,8 +18,11 @@ android { } } buildFeatures { - buildConfig true - aidl true + buildConfig = true + aidl = true + } + lint { + disable 'UseRequiresApi' } } diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 8bb8632b..435ac8f6 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.2 +SCRCPY_VERSION_NAME=3.3.4 -PLATFORM=${ANDROID_PLATFORM:-35} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} +PLATFORM=${ANDROID_PLATFORM:-36} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.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 2b331175..0d1f1979 100644 --- a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl +++ b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl @@ -48,19 +48,4 @@ 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 51db985c..a5816c32 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.BatteryManager; +import android.os.Looper; import android.system.ErrnoException; import android.system.Os; @@ -179,6 +180,11 @@ public final class CleanUp { } } + @SuppressWarnings("deprecation") + private static void prepareMainLooper() { + Looper.prepareMainLooper(); + } + public static void main(String... args) { try { // Start a new session to avoid being terminated along with the server process on some devices @@ -188,6 +194,10 @@ 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 22fc6d49..5d41a8f3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,6 +2,7 @@ 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; @@ -11,6 +12,8 @@ import android.content.IContentProvider; import android.os.Binder; import android.os.Process; +import java.lang.reflect.Field; + public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; @@ -72,7 +75,7 @@ public final class FakeContext extends ContextWrapper { @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName("shell"); + builder.setPackageName(PACKAGE_NAME); return builder.build(); } @@ -87,8 +90,38 @@ 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 09cfd6cf..04e4a837 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -24,10 +24,13 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import android.annotation.SuppressLint; import android.os.Build; +import android.os.Looper; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -55,17 +58,7 @@ public final class Server { this.fatalError = true; } if (running == 0 || this.fatalError) { - notify(); - } - } - - synchronized void await() { - try { - while (running > 0 && !fatalError) { - wait(); - } - } catch (InterruptedException e) { - // ignore + Looper.getMainLooper().quitSafely(); } } } @@ -172,7 +165,7 @@ public final class Server { }); } - completion.await(); + Looper.loop(); // interrupted by the Completion implementation } finally { if (cleanUp != null) { cleanUp.interrupt(); @@ -201,6 +194,21 @@ public final class Server { } } + private static void prepareMainLooper() { + // Like Looper.prepareMainLooper(), but with quitAllowed set to true + Looper.prepare(); + synchronized (Looper.class) { + try { + @SuppressLint("DiscouragedPrivateApi") + Field field = Looper.class.getDeclaredField("sMainLooper"); + field.setAccessible(true); + field.set(null, Looper.myLooper()); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + } + public static void main(String... args) { int status = 0; try { @@ -217,10 +225,16 @@ 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 fb4c1389..1e012de6 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,8 +29,6 @@ public final class Workarounds { private static final Object ACTIVITY_THREAD; static { - prepareMainLooper(); - try { // ActivityThread activityThread = new ActivityThread(); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); @@ -77,19 +75,6 @@ public final class Workarounds { fillAppContext(); } - @SuppressWarnings("deprecation") - private static void prepareMainLooper() { - // Some devices internally create a Handler when creating an input Surface, causing an exception: - // "Can't create handler inside thread that has not called Looper.prepare()" - // - // - // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: - // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' - // on a null object reference" - // - Looper.prepareMainLooper(); - } - private static void fillAppInfo() { try { // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); @@ -118,10 +103,7 @@ public final class Workarounds { private static void fillAppContext() { try { - Application app = new Application(); - Field baseField = ContextWrapper.class.getDeclaredField("mBase"); - baseField.setAccessible(true); - baseField.set(app, FakeContext.get()); + Application app = Instrumentation.newApplication(Application.class, 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 e503ec61..830a7ec7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -112,8 +112,9 @@ public class ControlMessageReader { private ControlMessage parseInjectScrollEvent() throws IOException { Position position = parsePosition(); - float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); - float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); + // Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16]. + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5e64a4c5..b4a8e3ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; @@ -17,7 +18,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; @@ -114,22 +114,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Ln.w("Input events are not supported for secondary displays before Android 10"); } + // Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled) + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardAutosync) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager != null) { - clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { - @Override - public void dispatchPrimaryClipChanged() { - if (isSettingClipboard.get()) { - // This is a notification for the change we are currently applying, ignore it - return; - } - String text = Device.getClipboardText(); - if (text != null) { - DeviceMessage msg = DeviceMessage.createClipboard(text); - sender.send(msg); - } + clipboardManager.addPrimaryClipChangedListener(() -> { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + String text = Device.getClipboardText(); + if (text != null) { + DeviceMessage msg = DeviceMessage.createClipboard(text); + sender.send(msg); } }); } else { @@ -156,8 +154,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private UhidManager getUhidManager() { if (uhidManager == null) { - uhidManager = new UhidManager(sender); + int uhidDisplayId = displayId; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + if (displayId == Device.DISPLAY_ID_NONE) { + // Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be + // associated to the virtual display + try { + // Wait for at most 1 second until a virtual display id is known + DisplayData data = waitDisplayData(1000); + if (data != null) { + uhidDisplayId = data.virtualDisplayId; + } + } catch (InterruptedException e) { + // do nothing + } + } + } + + String displayUniqueId = null; + if (uhidDisplayId > 0) { + // Ignore Device.DISPLAY_ID_NONE and 0 (main display) + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId); + if (displayInfo != null) { + displayUniqueId = displayInfo.getUniqueId(); + } + } + uhidManager = new UhidManager(sender, displayUniqueId); } + return uhidManager; } @@ -699,7 +723,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (timeout < 0) { return null; } - displayDataAvailable.wait(timeout); + if (timeout > 0) { + displayDataAvailable.wait(timeout); + } data = displayData.get(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index c4867a3f..20532c0b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import android.os.HandlerThread; @@ -31,14 +32,20 @@ public final class UhidManager { private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) + // Must be unique across the system + private static final String INPUT_PORT = "scrcpy:" + Os.getpid(); + + private final String displayUniqueId; + private final ArrayMap fds = new ArrayMap<>(); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; private final MessageQueue queue; - public UhidManager(DeviceMessageSender sender) { + public UhidManager(DeviceMessageSender sender, String displayUniqueId) { this.sender = sender; + this.displayUniqueId = displayUniqueId; if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); @@ -52,15 +59,22 @@ public final class UhidManager { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { + // First UHID device added + boolean firstDevice = fds.isEmpty(); + FileDescriptor old = fds.put(id, fd); if (old != null) { Ln.w("Duplicate UHID id: " + id); close(old); } - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); + String phys = mustUseInputPort() ? INPUT_PORT : null; + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys); Os.write(fd, req, 0, req.length); + if (firstDevice) { + addUniqueIdAssociation(); + } registerUhidListener(id, fd); } catch (Exception e) { close(fd); @@ -148,7 +162,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { /* * struct uhid_event { * uint32_t type; @@ -170,17 +184,23 @@ public final class UhidManager { * } __attribute__((__packed__)); */ - byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); String actualName = name.isEmpty() ? "scrcpy" : name; - byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); - int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); - assert len <= 127; - buf.put(utf8Name, 0, len); - buf.put(empty, 0, 256 - len); + byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); + int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); + assert nameLen <= 127; + buf.put(nameBytes, 0, nameLen); + if (phys != null) { + buf.position(4 + 128); + byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII); + assert physBytes.length <= 63; + buf.put(physBytes); + } + + buf.position(4 + 256); buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(vendorId); @@ -219,15 +239,26 @@ public final class UhidManager { if (fd != null) { unregisterUhidListener(fd); close(fd); + + if (fds.isEmpty()) { + // Last UHID device removed + removeUniqueIdAssociation(); + } } else { Ln.w("Closing unknown UHID device: " + id); } } public void closeAll() { + if (fds.isEmpty()) { + return; + } + for (FileDescriptor fd : fds.values()) { close(fd); } + + removeUniqueIdAssociation(); } private static void close(FileDescriptor fd) { @@ -237,4 +268,20 @@ public final class UhidManager { Ln.e("Failed to close uhid: " + e.getMessage()); } } + + private boolean mustUseInputPort() { + return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null; + } + + private void addUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId); + } + } + + private void removeUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index cdd4bab9..8d26b7ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -7,16 +7,18 @@ public final class DisplayInfo { private final int layerStack; private final int flags; private final int dpi; + private final String uniqueId; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; this.dpi = dpi; + this.uniqueId = uniqueId; } public int getDisplayId() { @@ -42,5 +44,8 @@ public final class DisplayInfo { public int getDpi() { return dpi; } -} + public String getUniqueId() { + return uniqueId; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java index c269750e..81168aae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java @@ -32,9 +32,11 @@ public enum Orientation { throw new IllegalArgumentException("Unknown orientation: " + name); } - public static Orientation fromRotation(int rotation) { - assert rotation >= 0 && rotation < 4; - return values()[rotation]; + public static Orientation fromRotation(int ccwRotation) { + assert ccwRotation >= 0 && ccwRotation < 4; + // Display rotation is expressed counter-clockwise, orientation is expressed clockwise + int cwRotation = (4 - ccwRotation) % 4; + return values()[cwRotation]; } public boolean isFlipped() { diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java index 86bd1859..06bcd7eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java @@ -1,6 +1,7 @@ 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; @@ -15,6 +16,7 @@ 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 { @@ -80,31 +82,17 @@ 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 { - sem.acquire(); - } catch (InterruptedException e) { - // Behave as if this method call was synchronous - Thread.currentThread().interrupt(); - } - - Throwable throwable = throwableRef[0]; - if (throwable != null) { + Threads.executeSynchronouslyOn(handler, new Callable() { + @Override + public Void call() throws Exception { + run(inputSize, outputSize, outputSurface); + return null; + } + }); + } catch (Throwable throwable) { 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 c0700125..811a4f31 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java @@ -74,9 +74,11 @@ public final class Ln { public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { Log.w(TAG, message, throwable); - CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); - if (throwable != null) { - throwable.printStackTrace(CONSOLE_ERR); + synchronized (CONSOLE_ERR) { + CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); + if (throwable != null) { + throwable.printStackTrace(CONSOLE_ERR); + } } } } @@ -88,9 +90,11 @@ public final class Ln { public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); - if (throwable != null) { - throwable.printStackTrace(CONSOLE_ERR); + synchronized (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 e6465525..4eb67d18 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,13 +1,8 @@ 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; @@ -18,66 +13,26 @@ public final class Settings { /* not instantiable */ } - 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); - } + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); } - - return execSettingsGet(table, key); } public static void putValue(String table, String key, String value) throws SettingsException { - 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); - } + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); } - execSettingsPut(table, key, value); } public static String getAndPutValue(String table, String key, String value) throws SettingsException { - 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); + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); } + 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 new file mode 100644 index 00000000..c561d1ef --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/util/Threads.java @@ -0,0 +1,43 @@ +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 792b3a8a..e933f60e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -25,6 +25,7 @@ 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; @@ -169,6 +170,7 @@ 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 791df0f8..54936122 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,270 +1,43 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; -import android.content.IOnPrimaryClipChangedListener; -import android.os.Build; -import android.os.IInterface; - -import java.lang.reflect.Method; +import android.content.Context; public final class ClipboardManager { - private final IInterface manager; - private Method getPrimaryClipMethod; - private Method setPrimaryClipMethod; - private Method addPrimaryClipChangedListener; - private int getMethodVersion; - private int setMethodVersion; - private int addListenerMethodVersion; + private final android.content.ClipboardManager manager; static ClipboardManager create() { - IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); - if (clipboard == null) { + android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); + if (manager == null) { // Some devices have no clipboard manager // // return null; } - return new ClipboardManager(clipboard); + return new ClipboardManager(manager); } - private ClipboardManager(IInterface manager) { + private ClipboardManager(android.content.ClipboardManager manager) { this.manager = manager; } - private Method getGetPrimaryClipMethod() throws NoSuchMethodException { - if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - return getPrimaryClipMethod; - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - getMethodVersion = 0; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - getMethodVersion = 1; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 5; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); - getMethodVersion = 6; - } - return getPrimaryClipMethod; - } - - private Method getSetPrimaryClipMethod() throws NoSuchMethodException { - if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - return setPrimaryClipMethod; - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - setMethodVersion = 0; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e1) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - setMethodVersion = 1; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e2) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e3) { - // fall-through - } - - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); - setMethodVersion = 3; - } - return setPrimaryClipMethod; - } - - private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); - } - - switch (methodVersion) { - case 0: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - case 1: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - case 2: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - case 3: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); - case 4: - // The last boolean parameter is "userOperate" - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - case 5: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); - default: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); - } - } - - private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - case 2: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - default: - // The last boolean parameter is "userOperate" - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - } - } - public CharSequence getText() { - try { - Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); - if (clipData == null || clipData.getItemCount() == 0) { - return null; - } - return clipData.getItemAt(0).getText(); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + ClipData clipData = manager.getPrimaryClip(); + if (clipData == null || clipData.getItemCount() == 0) { return null; } + return clipData.getItemAt(0).getText(); } public boolean setText(CharSequence text) { - try { - Method method = getSetPrimaryClipMethod(); - ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, setMethodVersion, manager, clipData); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + ClipData clipData = ClipData.newPlainText(null, text); + manager.setPrimaryClip(clipData); + return true; } - private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) - throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - default: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - } - } - - private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { - if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); - } else { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); - addListenerMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class); - addListenerMethodVersion = 1; - } catch (NoSuchMethodException e2) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class, int.class); - addListenerMethodVersion = 2; - } - } - } - } - return addPrimaryClipChangedListener; - } - - public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { - try { - Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { + manager.addPrimaryClipChangedListener(listener); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index d44ac608..9e84ec00 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -46,6 +46,7 @@ public final class DisplayManager { } private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal + private Method getDisplayInfoMethod; private Method createVirtualDisplayMethod; private Method requestDisplayPowerMethod; @@ -81,7 +82,7 @@ public final class DisplayManager { int density = Integer.parseInt(m.group(5)); int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -95,12 +96,12 @@ public final class DisplayManager { } private static int parseDisplayFlags(String text) { - Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); if (text == null) { return 0; } int flags = 0; + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); Matcher m = regex.matcher(text); while (m.find()) { String flagString = m.group(); @@ -114,9 +115,18 @@ public final class DisplayManager { return flags; } + // getDisplayInfo() may be used from both the Controller thread and the video (main) thread + private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException { + if (getDisplayInfoMethod == null) { + getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); + } + return getDisplayInfoMethod; + } + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + Method method = getGetDisplayInfoMethod(); + Object displayInfo = method.invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); @@ -129,7 +139,14 @@ public final class DisplayManager { int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); + String uniqueId; + 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); } 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 f2ecb158..1573d817 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java @@ -1,10 +1,11 @@ package com.genymobile.scrcpy.wrappers; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.view.IDisplayWindowListener; +import com.genymobile.scrcpy.util.Ln; -import java.util.List; +import android.content.res.Configuration; +import android.os.Parcel; +import android.os.RemoteException; +import android.view.IDisplayWindowListener; public class DisplayWindowListener extends IDisplayWindowListener.Stub { @Override @@ -23,17 +24,14 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub { } @Override - 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 + 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; + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 5c5ba56c..f55648d5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,11 +1,15 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.view.InputEvent; import android.view.MotionEvent; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -15,39 +19,28 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final Object manager; - private Method injectInputEventMethod; + private final android.hardware.input.InputManager manager; + private long lastPermissionLogDate; + private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; + private static Method addUniqueIdAssociationByPortMethod; + private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { - try { - Class inputManagerClass = getInputManagerClass(); - Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); - Object im = getInstanceMethod.invoke(null); - return new InputManager(im); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } + android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() + .getSystemService(FakeContext.INPUT_SERVICE); + return new InputManager(manager); } - private static Class getInputManagerClass() { - try { - // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview - return Class.forName("android.hardware.input.InputManagerGlobal"); - } catch (ClassNotFoundException e) { - return android.hardware.input.InputManager.class; - } - } - - private InputManager(Object manager) { + private InputManager(android.hardware.input.InputManager manager) { this.manager = manager; } - private Method getInjectInputEventMethod() throws NoSuchMethodException { + private static Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } @@ -57,6 +50,23 @@ public final class InputManager { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); } catch (ReflectiveOperationException e) { + if (e instanceof InvocationTargetException) { + Throwable cause = e.getCause(); + if (cause instanceof SecurityException) { + String message = e.getCause().getMessage(); + if (message != null && message.contains("INJECT_EVENTS permission")) { + // Do not flood the console, limit to one permission error log every 3 seconds + long now = System.currentTimeMillis(); + if (lastPermissionLogDate <= now - 3000) { + Ln.e(message); + Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device."); + lastPermissionLogDate = now; + } + // Do not print the stack trace + return false; + } + } + } Ln.e("Could not invoke method", e); return false; } @@ -97,4 +107,40 @@ public final class InputManager { return false; } } + + private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (addUniqueIdAssociationByPortMethod == null) { + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "addUniqueIdAssociationByPort", String.class, String.class); + } + return addUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) { + try { + Method method = getAddUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort, uniqueId); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot add unique id association by port", e); + } + } + + private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (removeUniqueIdAssociationByPortMethod == null) { + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "removeUniqueIdAssociationByPort", String.class); + } + return removeUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void removeUniqueIdAssociationByPort(String inputPort) { + try { + Method method = getRemoveUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot remove unique id association by port", e); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index a8a56dab..b1123b55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -54,7 +54,8 @@ public final class ServiceManager { return windowManager; } - public static DisplayManager getDisplayManager() { + // The DisplayManager may be used from both the Controller thread and the video (main) thread + public static synchronized DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = DisplayManager.create(); } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 74df064f..0cc0a6b5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -125,7 +125,7 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0); // 0.0f encoded as i16 - dos.writeShort(0x8000); // -1.0f encoded as i16 + dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16]) dos.writeInt(1); byte[] packet = bos.toByteArray(); @@ -139,7 +139,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(0f, event.getHScroll(), 0f); - Assert.assertEquals(-1f, event.getVScroll(), 0f); + Assert.assertEquals(-16f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(-1, bis.read()); // EOS