diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a77b7ff1..49402a6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,6 +74,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -83,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check architecture run: | @@ -99,6 +100,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -129,14 +131,12 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - name: Workaround for old meson version run by Github Actions - run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build run: release/build_windows.sh 32 @@ -162,14 +162,12 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - name: Workaround for old meson version run by Github Actions - run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build run: release/build_windows.sh 64 @@ -208,6 +206,13 @@ jobs: libtool - name: Build + env: + # the default Xcode (and macOS SDK) version can be found at + # + # + # then the minimal supported deployment target of that macOS SDK can be found at + # + MACOSX_DEPLOYMENT_TARGET: 10.13 run: release/build_macos.sh aarch64 # upload-artifact does not preserve permissions @@ -244,6 +249,13 @@ jobs: # autoconf and libtool are already installed on macos-13 - name: Build + env: + # the default Xcode (and macOS SDK) version can be found at + # + # + # then the minimal supported deployment target of that macOS SDK can be found at + # + MACOSX_DEPLOYMENT_TARGET: 10.13 run: release/build_macos.sh x86_64 # upload-artifact does not preserve permissions diff --git a/FAQ.md b/FAQ.md index 5f089cd7..24722c74 100644 --- a/FAQ.md +++ b/FAQ.md @@ -166,14 +166,13 @@ Rebooting the device is necessary once this option is set. ### Special characters do not work -The default text injection method is [limited to ASCII characters][text-input]. -A trick allows to also inject some [accented characters][accented-characters], +The default text injection method is limited to ASCII characters. A trick allows +to also inject some [accented characters][accented-characters], but that's all. See [#37]. To avoid the problem, [change the keyboard mode to simulate a physical keyboard][hid]. -[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [#37]: https://github.com/Genymobile/scrcpy/issues/37 [hid]: doc/keyboard.md#physical-keyboard-simulation diff --git a/LICENSE b/LICENSE index d9326a74..1196b3da 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2024 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 09fa12b4..d886d23c 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,16 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.1) +# scrcpy (v3.3.1) scrcpy _pronounced "**scr**een **c**o**py**"_ -This application mirrors Android devices (video and audio) connected via -USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the -device with the keyboard and the mouse of the computer. It does not require any -_root_ access. It works on _Linux_, _Windows_ and _macOS_. +This application mirrors Android devices (video and audio) connected via USB or +[TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the +computer's keyboard and mouse. It does not require _root_ access or an app +installed on the device. It works on _Linux_, _Windows_, and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) @@ -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 @@ -78,6 +78,16 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). - [macOS](doc/macos.md) +## Must-know tips + + - [Reducing resolution](doc/video.md#size) may greatly improve performance + (`scrcpy -m1024`) + - [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK` + - [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME` + - Alt+f toggles [fullscreen](doc/window.md#fullscreen) + - There are many other [shortcuts](doc/shortcuts.md) + + ## Usage examples There are a lot of options, [documented](#user-documentation) in separate pages. @@ -197,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-2024 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 29130892..a49da8ca 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { -d --select-usb --disable-screensaver --display-id= + --display-ime-policy= --display-orientation= -e --select-tcpip -f --fullscreen @@ -121,7 +122,7 @@ _scrcpy() { return ;; --audio-source) - COMPREPLY=($(compgen -W 'output mic playback' -- "$cur")) + COMPREPLY=($(compgen -W 'output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance' -- "$cur")) return ;; --camera-facing) @@ -148,6 +149,10 @@ _scrcpy() { COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; + --display-ime-policy) + COMPREPLY=($(compgen -W 'local fallback hide' -- "$cur")) + return + ;; --record-orientation) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return @@ -200,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 0897b9cc..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,12 +11,12 @@ 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]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' - '--audio-source=[Select the audio source]:source:(output mic playback)' + '--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-ar=[Select the camera size by its aspect ratio]' @@ -30,14 +30,15 @@ arguments=( {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display-id=[Specify the display id to mirror]' + '--display-ime-policy[Set the policy for selecting where the IME should be displayed]' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-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]' @@ -47,7 +48,7 @@ arguments=( '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' + '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh index 17b5641d..a3e339ec 100755 --- a/app/deps/adb_linux.sh +++ b/app/deps/adb_linux.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-linux.zip PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a +SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 cd "$SOURCES_DIR" diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh index 8a25915e..36f5df89 100755 --- a/app/deps/adb_macos.sh +++ b/app/deps/adb_macos.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-darwin.zip PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 +SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd cd "$SOURCES_DIR" diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh index d36706b0..de37162c 100755 --- a/app/deps/adb_windows.sh +++ b/app/deps/adb_windows.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-win.zip PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 +SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 cd "$SOURCES_DIR" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index d268ca91..fb8b9a25 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=7.1 +VERSION=7.1.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 +SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 cd "$SOURCES_DIR" diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 340b0f70..887a2a77 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=1.0.27 +VERSION=1.0.29 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de +SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 cd "$SOURCES_DIR" diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c098e367..54fee12b 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.30.10 +VERSION=2.32.8 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168 +SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c cd "$SOURCES_DIR" diff --git a/app/meson.build b/app/meson.build index be02ebc1..f7df69eb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -192,19 +192,19 @@ datadir = get_option('datadir') # by default 'share' install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', - install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) + install_dir: datadir / 'icons/hicolor/256x256/apps') install_data('data/zsh-completion/_scrcpy', - install_dir: join_paths(datadir, 'zsh/site-functions')) + install_dir: datadir / 'zsh/site-functions') install_data('data/bash-completion/scrcpy', - install_dir: join_paths(datadir, 'bash-completion/completions')) + install_dir: datadir / 'bash-completion/completions') # Desktop entry file for application launchers if host_machine.system() == 'linux' # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) install_data('data/scrcpy.desktop', - install_dir: join_paths(datadir, 'applications')) + install_dir: datadir / 'applications') install_data('data/scrcpy-console.desktop', - install_dir: join_paths(datadir, 'applications')) + install_dir: datadir / 'applications') endif @@ -279,3 +279,9 @@ if get_option('buildtype') == 'debug' test(t[0], exe) endforeach endif + +if meson.version().version_compare('>= 0.58.0') + devenv = environment() + devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png') + meson.add_devenv(devenv) +endif diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 2c441aa1..9c5374ae 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.1" + VALUE "ProductVersion", "3.3.1" END END BLOCK "VarFileInfo" diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 924905e4..d72fda13 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -67,13 +67,19 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source -Select the audio source (output, mic or playback). +Select the audio source. Possible values are: -The "output" source forwards the whole audio output, and disables playback on the device. - -The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). - -The "mic" source captures the microphone. + - "output": forwards the whole audio output, and disables playback on the device. + - "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - "mic": captures the microphone. + - "mic-unprocessed": captures the microphone unprocessed (raw) sound. + - "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available. + - "mic-voice-recognition": captures the microphone tuned for voice recognition. + - "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available). + - "voice-call": captures voice call. + - "voice-call-uplink": captures voice call uplink only. + - "voice-call-downlink": captures voice call downlink only. + - "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback. Default is output. @@ -161,6 +167,19 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. +.TP +.BI "\-\-display\-ime\-policy " value +Set the policy for selecting where the IME should be displayed. + +Possible values are "local", "fallback" and "hide": + + - "local" means that the IME should appear on the local display. + - "fallback" means that the IME should appear on a fallback display (the default display). + - "hide" means that the IME should be hidden. + +By default, the IME policy is left unchanged. + + .TP .BI "\-\-display\-orientation " value Set the initial display orientation. @@ -389,7 +408,7 @@ Disable video playback on the computer. .TP .B \-\-no\-window -Disable scrcpy window. Implies --no-video-playback and --no-control. +Disable scrcpy window. Implies --no-video-playback. .TP .BI "\-\-orientation " value @@ -491,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". @@ -829,7 +852,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2024 Romain Vimont +Copyright \(co 2018\-2025 Romain Vimont Licensed under the Apache License, Version 2.0. diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 0cd3c0fd..9e9cfd6b 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -4,9 +4,10 @@ #include #include #include +#include -#include "adb_device.h" -#include "adb_parser.h" +#include "adb/adb_device.h" +#include "adb/adb_parser.h" #include "util/env.h" #include "util/file.h" #include "util/log.h" @@ -109,7 +110,7 @@ show_adb_installation_msg(void) { } pkg_managers[] = { {"apt", "apt install adb"}, {"apt-get", "apt-get install adb"}, - {"brew", "brew cask install android-platform-tools"}, + {"brew", "brew install --cask android-platform-tools"}, {"dnf", "dnf install android-tools"}, {"emerge", "emerge dev-util/android-tools"}, {"pacman", "pacman -S android-tools"}, diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 43310fb9..e4903902 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -6,7 +6,7 @@ #include #include -#include "adb_device.h" +#include "adb/adb_device.h" #include "util/intr.h" #define SC_ADB_NO_STDOUT (1 << 0) diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index 56393bcf..308663ef 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "util/vector.h" diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 66bb1854..90a1b30b 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -3,6 +3,7 @@ #include #include #include +#include #include "util/log.h" #include "util/str.h" diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index f20349f6..b8738a35 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include -#include "adb_device.h" +#include "adb/adb_device.h" /** * Parse the available devices from the output of `adb devices` diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c index fa936e4b..43e80e13 100644 --- a/app/src/adb/adb_tunnel.c +++ b/app/src/adb/adb_tunnel.c @@ -1,11 +1,11 @@ #include "adb_tunnel.h" #include +#include -#include "adb.h" +#include "adb/adb.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process_intr.h" static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 9133c24a..5a66d43b 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -3,9 +3,7 @@ #include "common.h" -#include -#include -#include +#include #include "audio_regulator.h" #include "trait/frame_sink.h" diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 3e4f78ad..16fdd08b 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -1,5 +1,9 @@ #include "audio_regulator.h" +#include +#include +#include +#include #include #include @@ -72,8 +76,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, // Wait until the buffer is filled up to at least target_buffering // before playing if (buffered_samples < ar->target_buffering) { - LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] Inserting initial buffering silence: %" PRIu32 " samples", out_samples); +#endif // Delay playback starting to reach the target buffering. Fill the // whole buffer with silence (len is small compared to the // arbitrary margin value). @@ -94,8 +100,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, // dropped to keep the latency minimal. However, this would cause very // audible glitches, so let the clock compensation restore the target // latency. +#ifdef SC_AUDIO_REGULATOR_DEBUG LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", silence); +#endif memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); bool received = atomic_load_explicit(&ar->received, @@ -133,6 +141,36 @@ bool sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { SwrContext *swr_ctx = ar->swr_ctx; + uint32_t input_samples = frame->nb_samples; + + assert(frame->pts >= 0); + int64_t pts = frame->pts; + if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) { + LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs", + pts - ar->next_expected_pts); + // More than 100ms: consider it as a discontinuity + // (typically because silence packets were not captured) + uint32_t can_read = sc_audiobuf_can_read(&ar->buf); + if (input_samples + can_read < ar->target_buffering) { + // Adjust buffering to the target value directly + uint32_t silence = ar->target_buffering - can_read - input_samples; + sc_audiobuf_write_silence(&ar->buf, silence); + } + + // Reset state + ar->avg_buffering.avg = ar->target_buffering; + int ret = swr_set_compensation(swr_ctx, 0, 0); + (void) ret; + assert(!ret); // disabling compensation should never fail + ar->compensation_active = false; + ar->samples_since_resync = 0; + atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed); + } + + int64_t packet_duration = input_samples * INT64_C(1000000) + / ar->sample_rate; + ar->next_expected_pts = pts + packet_duration; + int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); // No need to av_rescale_rnd(), input and output sample rates are the same. // Add more space (256) for clock compensation. @@ -205,6 +243,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { if (played) { underflow = atomic_exchange_explicit(&ar->underflow, 0, memory_order_relaxed); + ar->underflow_report += underflow; max_buffered_samples = ar->target_buffering * 11 / 10 + 60 * ar->sample_rate / 1000 /* 60 ms */; @@ -251,7 +290,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { } // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - frame->nb_samples; + int32_t instant_compensation = (int32_t) written - input_samples; // Inserting silence instantly increases buffering int32_t inserted_silence = (int32_t) underflow; // Dropping input samples instantly decreases buffering @@ -307,7 +346,9 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { int abs_max_diff = distance / 50; diff = CLAMP(diff, -abs_max_diff, abs_max_diff); LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ar->target_buffering, avg, can_read, diff); + " compensation=%d (underflow=%" PRIu32 ")", + ar->target_buffering, avg, can_read, diff, ar->underflow_report); + ar->underflow_report = 0; int ret = swr_set_compensation(swr_ctx, diff, distance); if (ret < 0) { @@ -390,7 +431,9 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->played, false); atomic_init(&ar->received, false); atomic_init(&ar->underflow, 0); + ar->underflow_report = 0; ar->compensation_active = false; + ar->next_expected_pts = 0; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 1c0eeb9f..4e18fe08 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include "util/audiobuf.h" @@ -44,6 +46,9 @@ struct sc_audio_regulator { // Number of silence samples inserted since the last received packet atomic_uint_least32_t underflow; + // Number of silence samples inserted since the last log + uint32_t underflow_report; + // Non-zero compensation applied (only used by the receiver thread) bool compensation_active; @@ -52,6 +57,9 @@ struct sc_audio_regulator { // Set to true the first time samples are pulled by the player atomic_bool played; + + // PTS of the next expected packet (useful to detect discontinuities) + int64_t next_expected_pts; }; bool diff --git a/app/src/cli.c b/app/src/cli.c index ed1970d4..b2e3e30a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "options.h" @@ -13,6 +14,7 @@ #include "util/str.h" #include "util/strbuf.h" #include "util/term.h" +#include "util/tick.h" #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) @@ -111,6 +113,7 @@ enum { OPT_ANGLE, OPT_NO_VD_SYSTEM_DECORATIONS, OPT_NO_VD_DESTROY_CONTENT, + OPT_DISPLAY_IME_POLICY, }; struct sc_option { @@ -214,13 +217,31 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_SOURCE, .longopt = "audio-source", .argdesc = "source", - .text = "Select the audio source (output, mic or playback).\n" - "The \"output\" source forwards the whole audio output, and " - "disables playback on the device.\n" - "The \"playback\" source captures the audio playback (Android " - "apps can opt-out, so the whole output is not necessarily " + .text = "Select the audio source. Possible values are:\n" + " - \"output\": forwards the whole audio output, and disables " + "playback on the device.\n" + " - \"playback\": captures the audio playback (Android apps " + "can opt-out, so the whole output is not necessarily " "captured).\n" - "The \"mic\" source captures the microphone.\n" + " - \"mic\": captures the microphone.\n" + " - \"mic-unprocessed\": captures the microphone unprocessed " + "(raw) sound.\n" + " - \"mic-camcorder\": captures the microphone tuned for video " + "recording, with the same orientation as the camera if " + "available.\n" + " - \"mic-voice-recognition\": captures the microphone tuned " + "for voice recognition.\n" + " - \"mic-voice-communication\": captures the microphone tuned " + "for voice communications (it will for instance take advantage " + "of echo cancellation or automatic gain control if " + "available).\n" + " - \"voice-call\": captures voice call.\n" + " - \"voice-call-uplink\": captures voice call uplink only.\n" + " - \"voice-call-downlink\": captures voice call downlink " + "only.\n" + " - \"voice-performance\": captures audio meant to be " + "processed for live performance (karaoke), includes both the " + "microphone and the device playback.\n" "Default is output.", }, { @@ -364,6 +385,19 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_IME_POLICY, + .longopt = "display-ime-policy", + .argdesc = "value", + .text = "Set the policy for selecting where the IME should be " + "displayed.\n" + "Possible values are \"local\", \"fallback\" and \"hide\".\n" + "\"local\" means that the IME should appear on the local " + "display.\n" + "\"fallback\" means that the IME should appear on a fallback " + "display (the default display).\n" + "\"hide\" means that the IME should be hidden.", + }, { .longopt_id = OPT_DISPLAY_ORIENTATION, .longopt = "display-orientation", @@ -687,8 +721,7 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NO_WINDOW, .longopt = "no-window", - .text = "Disable scrcpy window. Implies --no-video-playback and " - "--no-control.", + .text = "Disable scrcpy window. Implies --no-video-playback.", }, { .longopt_id = OPT_ORIENTATION, @@ -1613,6 +1646,25 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } +static bool +parse_display_ime_policy(const char *s, enum sc_display_ime_policy *policy) { + if (!strcmp(s, "local")) { + *policy = SC_DISPLAY_IME_POLICY_LOCAL; + return true; + } + if (!strcmp(s, "fallback")) { + *policy = SC_DISPLAY_IME_POLICY_FALLBACK; + return true; + } + if (!strcmp(s, "hide")) { + *policy = SC_DISPLAY_IME_POLICY_HIDE; + return true; + } + LOGE("Unsupported display IME policy: %s (expected local, fallback or " + "hide)", s); + return false; +} + static bool parse_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -2002,8 +2054,50 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return true; } - LOGE("Unsupported audio source: %s (expected output, mic or playback)", - optarg); + if (!strcmp(optarg, "mic-unprocessed")) { + *source = SC_AUDIO_SOURCE_MIC_UNPROCESSED; + return true; + } + + if (!strcmp(optarg, "mic-camcorder")) { + *source = SC_AUDIO_SOURCE_MIC_CAMCORDER; + return true; + } + + if (!strcmp(optarg, "mic-voice-recognition")) { + *source = SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION; + return true; + } + + if (!strcmp(optarg, "mic-voice-communication")) { + *source = SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION; + return true; + } + + if (!strcmp(optarg, "voice-call")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL; + return true; + } + + if (!strcmp(optarg, "voice-call-uplink")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL_UPLINK; + return true; + } + + if (!strcmp(optarg, "voice-call-downlink")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK; + return true; + } + + if (!strcmp(optarg, "voice-performance")) { + *source = SC_AUDIO_SOURCE_VOICE_PERFORMANCE; + return true; + } + + LOGE("Unsupported audio source: %s (expected output, mic, playback, " + "mic-unprocessed, mic-camcorder, mic-voice-recognition, " + "mic-voice-communication, voice-call, voice-call-uplink, " + "voice-call-downlink, voice-performance)", optarg); return false; } @@ -2721,6 +2815,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_VD_SYSTEM_DECORATIONS: opts->vd_system_decorations = false; break; + case OPT_DISPLAY_IME_POLICY: + if (!parse_display_ime_policy(optarg, + &opts->display_ime_policy)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2759,9 +2859,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif if (!opts->window) { - // Without window, there cannot be any video playback or control + // Without window, there cannot be any video playback opts->video_playback = false; - opts->control = false; + // Controls are still possible, allowing for options like + // --turn-screen-off } if (!opts->video) { @@ -2976,6 +3077,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { + LOGE("--display-ime-policy is only available with " + "--video-source=display"); + return false; + } + if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { LOGE("Cannot specify both --camera-id and --camera-facing"); return false; @@ -3017,6 +3124,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED + && opts->display_id == 0 && !opts->new_display) { + LOGE("--display-ime-policy is only supported on a secondary display"); + return false; + } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { 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/decoder.c b/app/src/decoder.c index 5d42b8b0..4d0a1daf 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,11 +1,9 @@ #include "decoder.h" -#include -#include -#include +#include +#include +#include -#include "events.h" -#include "trait/frame_sink.h" #include "util/log.h" /** Downcast packet_sink to decoder */ diff --git a/app/src/decoder.h b/app/src/decoder.h index ba8903f4..1f525fae 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -3,13 +3,11 @@ #include "common.h" +#include + #include "trait/frame_source.h" #include "trait/packet_sink.h" -#include -#include -#include - struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait struct sc_frame_source frame_source; // frame source trait diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index e89a2092..f75c6f72 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -2,9 +2,7 @@ #include #include - -#include -#include +#include #include "util/log.h" diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 18c1ce94..61cd77e4 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "clock.h" #include "trait/frame_source.h" diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 7223b553..885cd6ee 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -1,14 +1,11 @@ #include "demuxer.h" #include +#include +#include #include -#include -#include -#include "decoder.h" -#include "events.h" #include "packet_merger.h" -#include "recorder.h" #include "util/binary.h" #include "util/log.h" diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 5587d12d..2b7cb703 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -4,12 +4,8 @@ #include "common.h" #include -#include -#include -#include #include "trait/packet_source.h" -#include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 86b2ccb7..d6c701bb 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include #include -#include +#include #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes diff --git a/app/src/display.c b/app/src/display.c index 39018834..aee8ef80 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -1,6 +1,8 @@ #include "display.h" #include +#include +#include #include #include "util/log.h" diff --git a/app/src/display.h b/app/src/display.h index 064bb7bf..4de9b0a9 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -4,7 +4,8 @@ #include "common.h" #include -#include +#include +#include #include #include "coords.h" diff --git a/app/src/events.c b/app/src/events.c index ce885241..b4322d1b 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -1,5 +1,7 @@ #include "events.h" +#include + #include "util/log.h" #include "util/thread.h" diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 06911052..681fb5d6 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -1,11 +1,11 @@ #include "file_pusher.h" #include +#include #include #include "adb/adb.h" #include "util/log.h" -#include "util/process_intr.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index dd4ae1da..1daa42ba 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,6 +1,7 @@ #include "fps_counter.h" #include +#include #include "util/log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index e7619271..3eab461c 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -5,9 +5,9 @@ #include #include -#include #include "util/thread.h" +#include "util/tick.h" struct sc_fps_counter { sc_thread thread; diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index 5699b58f..9fd4cf6f 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -1,8 +1,6 @@ #include "frame_buffer.h" #include -#include -#include #include "util/log.h" diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h index f97261cd..e748adfb 100644 --- a/app/src/frame_buffer.h +++ b/app/src/frame_buffer.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "util/thread.h" diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index d6818e30..b0d45ce8 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #define SC_HID_MAX_SIZE 15 diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 8f4e4527..842eae9e 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include "util/binary.h" #include "util/log.h" diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h index b532a703..8d939ac7 100644 --- a/app/src/hid/hid_gamepad.h +++ b/app/src/hid/hid_gamepad.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 961ad790..6477396a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -1,5 +1,6 @@ #include "hid_keyboard.h" +#include #include #include "util/log.h" diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index cde1ac52..5ecfd8cf 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 7acc413b..33f0807e 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -1,8 +1,10 @@ #include "hid_mouse.h" +#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: @@ -73,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, @@ -158,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 @@ -170,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 a9a54718..4ae4bfd4 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -3,8 +3,6 @@ #include "common.h" -#include - #include "hid/hid_event.h" #include "input_events.h" @@ -24,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/icon.c b/app/src/icon.c index 4f3a9a39..797afc75 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -2,17 +2,22 @@ #include #include +#include +#include +#include #include #include +#include #include #include +#include #include "config.h" -#include "compat.h" #include "util/env.h" -#include "util/file.h" +#ifdef PORTABLE +# include "util/file.h" +#endif #include "util/log.h" -#include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ diff --git a/app/src/icon.h b/app/src/icon.h index 3251e48f..6bcf46d2 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -3,9 +3,7 @@ #include "common.h" -#include -#include -#include +#include SDL_Surface * scrcpy_icon_load(void); diff --git a/app/src/input_events.h b/app/src/input_events.h index ad3afa81..1e34b50e 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -9,7 +9,6 @@ #include #include "coords.h" -#include "options.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. @@ -394,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 2e4337db..3e4dd0f3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,8 +1,12 @@ #include "input_manager.h" #include -#include +#include +#include +#include +#include "android/input.h" +#include "android/keycodes.h" #include "input_events.h" #include "screen.h" #include "shortcut_mod.h" @@ -893,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/input_manager.h b/app/src/input_manager.h index 8efd0153..af4cbc69 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -4,12 +4,12 @@ #include "common.h" #include - -#include +#include +#include +#include #include "controller.h" #include "file_pusher.h" -#include "fps_counter.h" #include "options.h" #include "trait/gamepad_processor.h" #include "trait/key_processor.h" diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 2d9ca85b..466a1aeb 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -1,8 +1,13 @@ #include "keyboard_sdk.h" #include +#include +#include +#include +#include #include "android/input.h" +#include "android/keycodes.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" diff --git a/app/src/main.c b/app/src/main.c index 8bbd074f..c58e0be7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,9 +1,6 @@ #include "common.h" -#include #include -#include -#include #ifdef HAVE_V4L2 # include #endif diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index a7998972..7eceffa7 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -1,12 +1,12 @@ #include "mouse_sdk.h" #include +#include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" -#include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_sdk */ diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h index 142b89bb..fe92a2d7 100644 --- a/app/src/mouse_sdk.h +++ b/app/src/mouse_sdk.h @@ -6,7 +6,6 @@ #include #include "controller.h" -#include "screen.h" #include "trait/mouse_processor.h" struct sc_mouse_sdk { diff --git a/app/src/opengl.c b/app/src/opengl.c index 376690af..0cb83ed7 100644 --- a/app/src/opengl.c +++ b/app/src/opengl.c @@ -2,7 +2,8 @@ #include #include -#include "SDL2/SDL.h" +#include +#include void sc_opengl_init(struct sc_opengl *gl) { diff --git a/app/src/options.c b/app/src/options.c index df8033e9..0fe82d29 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -1,5 +1,7 @@ #include "options.h" +#include + const struct scrcpy_options scrcpy_options_default = { .serial = NULL, .crop = NULL, @@ -54,6 +56,7 @@ const struct scrcpy_options scrcpy_options_default = { .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, + .display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, diff --git a/app/src/options.h b/app/src/options.h index 152881d8..03b42913 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -5,7 +5,6 @@ #include #include -#include #include #include "util/tick.h" @@ -60,6 +59,14 @@ enum sc_audio_source { SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, SC_AUDIO_SOURCE_PLAYBACK, + SC_AUDIO_SOURCE_MIC_UNPROCESSED, + SC_AUDIO_SOURCE_MIC_CAMCORDER, + SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION, + SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION, + SC_AUDIO_SOURCE_VOICE_CALL, + SC_AUDIO_SOURCE_VOICE_CALL_UPLINK, + SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK, + SC_AUDIO_SOURCE_VOICE_PERFORMANCE, }; enum sc_camera_facing { @@ -90,6 +97,13 @@ enum sc_orientation_lock { SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation }; +enum sc_display_ime_policy { + SC_DISPLAY_IME_POLICY_UNDEFINED, + SC_DISPLAY_IME_POLICY_LOCAL, + SC_DISPLAY_IME_POLICY_FALLBACK, + SC_DISPLAY_IME_POLICY_HIDE, +}; + static inline bool sc_orientation_is_mirror(enum sc_orientation orientation) { assert(!(orientation & ~7)); @@ -252,6 +266,7 @@ struct scrcpy_options { enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; enum sc_orientation record_orientation; + enum sc_display_ime_policy display_ime_policy; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c index 81b02d2c..dea038b6 100644 --- a/app/src/packet_merger.c +++ b/app/src/packet_merger.c @@ -1,5 +1,9 @@ #include "packet_merger.h" +#include +#include +#include + #include "util/log.h" void diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h index e1824c2c..3f9972ce 100644 --- a/app/src/packet_merger.h +++ b/app/src/packet_merger.h @@ -5,7 +5,7 @@ #include #include -#include +#include /** * Config packets (containing the SPS/PPS) are sent in-band. A new config diff --git a/app/src/receiver.c b/app/src/receiver.c index b89b0c6e..2ccb8a8b 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -2,7 +2,6 @@ #include #include -#include #include #include "device_msg.h" diff --git a/app/src/recorder.c b/app/src/recorder.c index 15f27157..c26f8f2d 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,6 +1,9 @@ #include "recorder.h" #include +#include +#include +#include #include #include #include diff --git a/app/src/recorder.h b/app/src/recorder.h index d096e79a..70b73836 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -4,9 +4,10 @@ #include "common.h" #include +#include +#include #include -#include "coords.h" #include "options.h" #include "trait/packet_sink.h" #include "util/thread.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f1942e43..a4c8c340 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -1,10 +1,11 @@ #include "scrcpy.h" +#include +#include +#include #include +#include #include -#include -#include -#include #include #ifdef _WIN32 @@ -37,9 +38,9 @@ #endif #include "util/acksync.h" #include "util/log.h" -#include "util/net.h" #include "util/rand.h" #include "util/timeout.h" +#include "util/tick.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif @@ -106,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"); @@ -164,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) { @@ -196,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; @@ -435,6 +447,7 @@ scrcpy(struct scrcpy_options *options) { .control = options->control, .display_id = options->display_id, .new_display = options->new_display, + .display_ime_policy = options->display_ime_policy, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, @@ -931,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/scrcpy.h b/app/src/scrcpy.h index d4d494a3..7f6a0fb2 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include "options.h" enum scrcpy_exit_code { diff --git a/app/src/screen.h b/app/src/screen.h index c716c399..6621b2d2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,11 +1,14 @@ -#ifndef SCREEN_H -#define SCREEN_H +#ifndef SC_SCREEN_H +#define SC_SCREEN_H #include "common.h" #include +#include #include -#include +#include +#include +#include #include "controller.h" #include "coords.h" @@ -14,7 +17,6 @@ #include "frame_buffer.h" #include "input_manager.h" #include "mouse_capture.h" -#include "opengl.h" #include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" diff --git a/app/src/server.c b/app/src/server.c index 22ddd372..153219c3 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,19 +1,18 @@ #include "server.h" #include -#include #include #include -#include -#include +#include +#include +#include #include "adb/adb.h" -#include "util/binary.h" #include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process_intr.h" +#include "util/process.h" #include "util/str.h" #define SC_SERVER_FILENAME "scrcpy-server" @@ -150,12 +149,43 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { return "mic"; case SC_AUDIO_SOURCE_PLAYBACK: return "playback"; + case SC_AUDIO_SOURCE_MIC_UNPROCESSED: + return "mic-unprocessed"; + case SC_AUDIO_SOURCE_MIC_CAMCORDER: + return "mic-camcorder"; + case SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION: + return "mic-voice-recognition"; + case SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION: + return "mic-voice-communication"; + case SC_AUDIO_SOURCE_VOICE_CALL: + return "voice-call"; + case SC_AUDIO_SOURCE_VOICE_CALL_UPLINK: + return "voice-call-uplink"; + case SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK: + return "voice-call-downlink"; + case SC_AUDIO_SOURCE_VOICE_PERFORMANCE: + return "voice-performance"; default: assert(!"unexpected audio source"); return NULL; } } +static const char * +sc_server_get_display_ime_policy_name(enum sc_display_ime_policy policy) { + switch (policy) { + case SC_DISPLAY_IME_POLICY_LOCAL: + return "local"; + case SC_DISPLAY_IME_POLICY_FALLBACK: + return "fallback"; + case SC_DISPLAY_IME_POLICY_HIDE: + return "hide"; + default: + assert(!"unexpected display IME policy"); + return NULL; + } +} + static bool validate_string(const char *s) { // The parameters values are passed as command line arguments to adb, so @@ -377,6 +407,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { + ADD_PARAM("display_ime_policy=%s", + sc_server_get_display_ime_policy_name(params->display_ime_policy)); + } if (!params->vd_destroy_content) { ADD_PARAM("vd_destroy_content=false"); } diff --git a/app/src/server.h b/app/src/server.h index 3c78b9ed..5f4592de 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,19 +1,17 @@ -#ifndef SERVER_H -#define SERVER_H +#ifndef SC_SERVER_H +#define SC_SERVER_H #include "common.h" -#include #include #include #include "adb/adb_tunnel.h" -#include "coords.h" #include "options.h" #include "util/intr.h" -#include "util/log.h" #include "util/net.h" #include "util/thread.h" +#include "util/tick.h" #define SC_DEVICE_NAME_FIELD_LENGTH 64 struct sc_server_info { @@ -52,6 +50,7 @@ struct sc_server_params { bool control; uint32_t display_id; const char *new_display; + enum sc_display_ime_policy display_ime_policy; bool video; bool audio; bool audio_dup; diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h index b685e987..f6c13f03 100644 --- a/app/src/shortcut_mod.h +++ b/app/src/shortcut_mod.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 6123c788..8f7fb074 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -1,10 +1,11 @@ #include "util/file.h" #include -#include #include +#include #include #include +#include #include #ifdef __APPLE__ # include // for _NSGetExecutablePath() diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 8c4a53c3..36d1ff7d 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 8ef248b6..67be4d46 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c index 416eccd9..56848309 100644 --- a/app/src/trait/frame_source.c +++ b/app/src/trait/frame_source.c @@ -1,5 +1,7 @@ #include "frame_source.h" +#include + void sc_frame_source_init(struct sc_frame_source *source) { source->sink_count = 0; diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h index 94222af0..cb1ef905 100644 --- a/app/src/trait/frame_source.h +++ b/app/src/trait/frame_source.h @@ -3,7 +3,9 @@ #include "common.h" -#include "frame_sink.h" +#include + +#include "trait/frame_sink.h" #define SC_FRAME_SOURCE_MAX_SINKS 2 diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h index 19629a9a..5e8dc2a4 100644 --- a/app/src/trait/gamepad_processor.h +++ b/app/src/trait/gamepad_processor.h @@ -3,9 +3,6 @@ #include "common.h" -#include -#include - #include "input_events.h" /** diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 96374413..9e9bb86e 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include "input_events.h" diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index 6e0b596e..d0a96e7c 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include "input_events.h" diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index 84cfe814..e12dea12 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c index c0836f1d..0a2c6c4d 100644 --- a/app/src/trait/packet_source.c +++ b/app/src/trait/packet_source.c @@ -1,5 +1,7 @@ #include "packet_source.h" +#include + void sc_packet_source_init(struct sc_packet_source *source) { source->sink_count = 0; diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h index 16d56e86..8788021a 100644 --- a/app/src/trait/packet_source.h +++ b/app/src/trait/packet_source.h @@ -3,7 +3,9 @@ #include "common.h" -#include "packet_sink.h" +#include + +#include "trait/packet_sink.h" #define SC_PACKET_SOURCE_MAX_SINKS 2 diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index a066cf03..c64feb18 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -1,5 +1,10 @@ #include "gamepad_uhid.h" +#include +#include +#include +#include + #include "hid/hid_gamepad.h" #include "input_events.h" #include "util/log.h" diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h index 07d03099..ad747604 100644 --- a/app/src/uhid/gamepad_uhid.h +++ b/app/src/uhid/gamepad_uhid.h @@ -3,8 +3,6 @@ #include "common.h" -#include - #include "controller.h" #include "hid/hid_gamepad.h" #include "trait/gamepad_processor.h" diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 76d70cc5..70082990 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -1,6 +1,12 @@ #include "keyboard_uhid.h" +#include +#include +#include +#include + #include "util/log.h" +#include "util/thread.h" /** Downcast key processor to keyboard_uhid */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 471030e7..869e48a4 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -1,5 +1,8 @@ #include "mouse_uhid.h" +#include +#include + #include "hid/hid_mouse.h" #include "input_events.h" #include "util/log.h" @@ -52,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/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index 05e691da..e743a73c 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,6 +1,5 @@ #include "uhid_output.h" -#include #include #include "uhid/keyboard_uhid.h" diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index cd6a800f..ed028b58 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -3,7 +3,7 @@ #include "common.h" -#include +#include #include /** diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 236a78ed..8cb62bfd 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,13 +1,16 @@ -#include "util/log.h" +#include "aoa_hid.h" #include #include #include +#include +#include +#include -#include "aoa_hid.h" #include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/tick.h" #include "util/vector.h" // See . diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 9cc6355e..2755c957 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -3,16 +3,13 @@ #include "common.h" -#include #include - -#include +#include #include "hid/hid_event.h" -#include "usb.h" +#include "usb/usb.h" #include "util/acksync.h" #include "util/thread.h" -#include "util/tick.h" #include "util/vecdeque.h" enum sc_aoa_event_type { diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c index 4372379f..d29b1a78 100644 --- a/app/src/usb/gamepad_aoa.c +++ b/app/src/usb/gamepad_aoa.c @@ -1,5 +1,7 @@ #include "gamepad_aoa.h" +#include + #include "input_events.h" #include "util/log.h" diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h index b2dfbe5e..0297a365 100644 --- a/app/src/usb/gamepad_aoa.h +++ b/app/src/usb/gamepad_aoa.h @@ -3,10 +3,8 @@ #include "common.h" -#include - -#include "aoa_hid.h" #include "hid/hid_gamepad.h" +#include "usb/aoa_hid.h" #include "trait/gamepad_processor.h" struct sc_gamepad_aoa { diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h index 565b9177..9e9500a3 100644 --- a/app/src/usb/keyboard_aoa.h +++ b/app/src/usb/keyboard_aoa.h @@ -5,8 +5,8 @@ #include -#include "aoa_hid.h" #include "hid/hid_keyboard.h" +#include "usb/aoa_hid.h" #include "trait/key_processor.h" struct sc_keyboard_aoa { diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index cb566cc0..fd5fa5e0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -1,6 +1,7 @@ #include "mouse_aoa.h" #include +#include #include "hid/hid_mouse.h" #include "input_events.h" @@ -41,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/mouse_aoa.h b/app/src/usb/mouse_aoa.h index afaed761..506286ba 100644 --- a/app/src/usb/mouse_aoa.h +++ b/app/src/usb/mouse_aoa.h @@ -5,7 +5,7 @@ #include -#include "aoa_hid.h" +#include "usb/aoa_hid.h" #include "trait/mouse_processor.h" struct sc_mouse_aoa { diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 6ef2fc2a..1a9cc46e 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -1,10 +1,19 @@ #include "scrcpy_otg.h" +#include +#include +#include #include -#include "adb/adb.h" +#ifdef _WIN32 +# include "adb/adb.h" +#endif #include "events.h" -#include "screen_otg.h" +#include "usb/screen_otg.h" +#include "usb/aoa_hid.h" +#include "usb/gamepad_aoa.h" +#include "usb/keyboard_aoa.h" +#include "usb/mouse_aoa.h" #include "util/log.h" struct scrcpy_otg { diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 368af125..5c580df9 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -1,7 +1,11 @@ #include "screen_otg.h" +#include +#include + #include "icon.h" #include "options.h" +#include "util/acksync.h" #include "util/log.h" static void @@ -160,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/usb/screen_otg.h b/app/src/usb/screen_otg.h index 427723ad..08b76ae7 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -4,12 +4,13 @@ #include "common.h" #include +#include #include -#include "keyboard_aoa.h" -#include "mouse_aoa.h" #include "mouse_capture.h" -#include "gamepad_aoa.h" +#include "usb/gamepad_aoa.h" +#include "usb/keyboard_aoa.h" +#include "usb/mouse_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c index 2899cdcb..76ecee0d 100644 --- a/app/src/util/acksync.c +++ b/app/src/util/acksync.c @@ -1,7 +1,6 @@ #include "acksync.h" #include -#include "util/log.h" bool sc_acksync_init(struct sc_acksync *as) { diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h index 58ab1b35..3d9c9b2f 100644 --- a/app/src/util/acksync.h +++ b/app/src/util/acksync.h @@ -3,7 +3,10 @@ #include "common.h" -#include "thread.h" +#include +#include +#include "util/thread.h" +#include "util/tick.h" #define SC_SEQUENCE_INVALID 0 diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c index 3cc5cad1..eeb27514 100644 --- a/app/src/util/audiobuf.c +++ b/app/src/util/audiobuf.c @@ -116,3 +116,38 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, return samples_count; } + +uint32_t +sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) { + // Only the writer thread can write head, so memory_order_relaxed is + // sufficient + uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); + + // The tail cursor is updated after the data is consumed by the reader + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + + uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (!can_write) { + return 0; + } + if (samples_count > can_write) { + samples_count = can_write; + } + + uint32_t right_count = buf->alloc_size - head; + if (right_count > samples_count) { + right_count = samples_count; + } + memset(buf->data + (head * buf->sample_size), 0, + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memset(buf->data, 0, left_count * buf->sample_size); + } + + uint32_t new_head = (head + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->head, new_head, memory_order_release); + + return samples_count; +} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 5e7dd4a0..b55a5a59 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -6,6 +6,7 @@ #include #include #include +#include #include /** @@ -49,6 +50,9 @@ uint32_t sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, uint32_t samples_count); +uint32_t +sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples); + static inline uint32_t sc_audiobuf_capacity(struct sc_audiobuf *buf) { assert(buf->alloc_size); diff --git a/app/src/util/average.h b/app/src/util/average.h index 59fae7d1..eded9987 100644 --- a/app/src/util/average.h +++ b/app/src/util/average.h @@ -3,9 +3,6 @@ #include "common.h" -#include -#include - struct sc_average { // Current average value float avg; diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 7de9b505..b6ce3201 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include static inline void diff --git a/app/src/util/env.c b/app/src/util/env.c index 1128e5ea..127f5a1f 100644 --- a/app/src/util/env.c +++ b/app/src/util/env.c @@ -2,7 +2,9 @@ #include #include -#include "util/str.h" +#ifdef _WIN32 +# include "util/str.h" +#endif char * sc_get_env(const char *varname) { diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h index 2898c461..7ab903ca 100644 --- a/app/src/util/intmap.h +++ b/app/src/util/intmap.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include struct sc_intmap_entry { diff --git a/app/src/util/intr.c b/app/src/util/intr.c index 22bd121a..ddf4839f 100644 --- a/app/src/util/intr.c +++ b/app/src/util/intr.c @@ -1,9 +1,9 @@ #include "intr.h" -#include "util/log.h" - #include +#include "util/log.h" + bool sc_intr_init(struct sc_intr *intr) { bool ok = sc_mutex_init(&intr->mutex); diff --git a/app/src/util/intr.h b/app/src/util/intr.h index 1c20f6df..35bd3375 100644 --- a/app/src/util/intr.h +++ b/app/src/util/intr.h @@ -6,9 +6,9 @@ #include #include -#include "net.h" -#include "process.h" -#include "thread.h" +#include "util/net.h" +#include "util/process.h" +#include "util/thread.h" /** * Interruptor to wake up a blocking call from another thread diff --git a/app/src/util/log.c b/app/src/util/log.c index 8a347c84..9114a258 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -4,7 +4,10 @@ # include #endif #include -#include +#include +#include +#include +#include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { diff --git a/app/src/util/net.c b/app/src/util/net.c index d68b0af6..9562ff6b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,28 +1,27 @@ #include "net.h" #include -#include #include -#include "log.h" - #ifdef _WIN32 # include typedef int socklen_t; #else -# include -# include +# include +# include # include # include -# include # include -# include +# include +# include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; #endif +#include "util/log.h" + bool net_init(void) { #ifdef _WIN32 diff --git a/app/src/util/net.h b/app/src/util/net.h index 94789954..aa99bbc4 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -4,14 +4,15 @@ #include "common.h" #include +#include #include +#include #ifdef _WIN32 # include typedef SOCKET sc_raw_socket; # define SC_RAW_SOCKET_NONE INVALID_SOCKET #else // not _WIN32 -# include typedef int sc_raw_socket; # define SC_RAW_SOCKET_NONE -1 #endif diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h index dbef528d..e2bbee88 100644 --- a/app/src/util/net_intr.h +++ b/app/src/util/net_intr.h @@ -3,8 +3,13 @@ #include "common.h" -#include "intr.h" -#include "net.h" +#include +#include +#include +#include + +#include "util/intr.h" +#include "util/net.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, diff --git a/app/src/util/process.c b/app/src/util/process.c index 9c4dcd9f..29d89a54 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,8 +1,6 @@ #include "process.h" #include -#include -#include "log.h" enum sc_process_result sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) { diff --git a/app/src/util/process.h b/app/src/util/process.h index 4d9d1684..eec51bcc 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -4,7 +4,9 @@ #include "common.h" #include +#include #include "util/thread.h" +#include "util/tick.h" #ifdef _WIN32 diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index d37bd5a5..641440ab 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -5,7 +5,7 @@ sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted - return false; + return -1; } ssize_t ret = sc_pipe_read(pipe, data, len); @@ -22,7 +22,7 @@ sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted - return false; + return -1; } ssize_t ret = sc_pipe_read_all(pipe, data, len); diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index 530a9046..020eafa1 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -3,8 +3,8 @@ #include "common.h" -#include "intr.h" -#include "process.h" +#include "util/intr.h" +#include "util/process.h" ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, diff --git a/app/src/util/str.c b/app/src/util/str.c index 304cd302..83d19c4d 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -12,8 +12,8 @@ # include #endif -#include "log.h" -#include "strbuf.h" +#include "util/log.h" +#include "util/strbuf.h" size_t sc_strncpy(char *dest, const char *src, size_t n) { diff --git a/app/src/util/str.h b/app/src/util/str.h index d20f1b28..b386b48d 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -5,6 +5,8 @@ #include #include +#include +#include /* Stringify a numeric value */ #define SC_STR(s) SC_XSTR(s) diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c index 1892b46b..6196d746 100644 --- a/app/src/util/strbuf.c +++ b/app/src/util/strbuf.c @@ -1,11 +1,10 @@ #include "strbuf.h" #include -#include #include #include -#include "log.h" +#include "util/log.h" bool sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 9679dfff..2a5253f7 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -1,10 +1,12 @@ #include "thread.h" #include +#include +#include #include #include -#include "log.h" +#include "util/log.h" sc_thread_id SC_MAIN_THREAD_ID; diff --git a/app/src/util/tick.c b/app/src/util/tick.c index cc0bab5e..edef1070 100644 --- a/app/src/util/tick.c +++ b/app/src/util/tick.c @@ -1,6 +1,7 @@ #include "tick.h" #include +#include #include #ifdef _WIN32 # include diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c index 159a4681..21bc3a53 100644 --- a/app/src/util/timeout.c +++ b/app/src/util/timeout.c @@ -1,8 +1,9 @@ #include "timeout.h" #include +#include -#include "log.h" +#include "util/log.h" bool sc_timeout_init(struct sc_timeout *timeout) { diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h index ae171b86..a45ae2ae 100644 --- a/app/src/util/timeout.h +++ b/app/src/util/timeout.h @@ -5,8 +5,8 @@ #include -#include "thread.h" -#include "tick.h" +#include "util/thread.h" +#include "util/tick.h" struct sc_timeout { sc_thread thread; diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index ce559ee9..e31724e2 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/app/src/util/vector.h b/app/src/util/vector.h index 97d7c389..5b399d56 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -5,8 +5,8 @@ #include #include +#include #include -#include // Adapted from vlc_vector: // diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 087e9af4..da9e02ef 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,5 +1,9 @@ #include "v4l2_sink.h" +#include +#include +#include +#include #include #include "util/log.h" diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 365a739d..2b7c5b50 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -3,13 +3,13 @@ #include "common.h" +#include #include #include -#include "coords.h" -#include "trait/frame_sink.h" #include "frame_buffer.h" -#include "util/tick.h" +#include "trait/frame_sink.h" +#include "util/thread.h" struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait diff --git a/app/src/version.c b/app/src/version.c index 90ea3334..f8610714 100644 --- a/app/src/version.c +++ b/app/src/version.c @@ -1,5 +1,6 @@ #include "version.h" +#include #include #include #include @@ -9,6 +10,7 @@ #ifdef HAVE_USB # include #endif +#include void scrcpy_print_version(void) { diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c index 94d0f07a..539ee238 100644 --- a/app/tests/test_audiobuf.c +++ b/app/tests/test_audiobuf.c @@ -113,6 +113,14 @@ static void test_audiobuf_partial_read_write(void) { uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; assert(!memcmp(data, expected2, 12)); + w = sc_audiobuf_write_silence(&buf, 4); + assert(w == 4); + + r = sc_audiobuf_read(&buf, data, 4); + assert(r == 4); + uint32_t expected3[] = {0, 0, 0, 0}; + assert(!memcmp(data, expected3, 4)); + sc_audiobuf_destroy(&buf); } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index af97182d..0d19919e 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) { .height = 1920, }, }, - .hscroll = 1, - .vscroll = -1, + .hscroll = 16, + .vscroll = -16, .buttons = 1, }, }; @@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x7F, 0xFF, // 1 (float encoded as i16) - 0x80, 0x00, // -1 (float encoded as i16) + 0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) + 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/cross_win32.txt b/cross_win32.txt index 05f9a86b..ddbc65f3 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -7,6 +7,8 @@ cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' pkg-config = 'i686-w64-mingw32-pkg-config' +# backward compatibility +pkgconfig = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] diff --git a/cross_win64.txt b/cross_win64.txt index 86364ad6..a6f16e16 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -7,6 +7,8 @@ cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' pkg-config = 'x86_64-w64-mingw32-pkg-config' +# backward compatibility +pkgconfig = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] diff --git a/doc/audio.md b/doc/audio.md index 85f76ac5..142626f5 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -66,6 +66,20 @@ the computer: scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ``` +Many sources are available: + + - `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)). + - `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)). + - `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)). + - `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)). + - `mic-voice-recognition`: captures the microphone tuned for voice recognition (mapped to [`VOICE_RECOGNITION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_RECOGNITION)). + - `mic-voice-communication`: captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available) (mapped to [`VOICE_COMMUNICATION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION)). + - `voice-call`: captures voice call (mapped to [`VOICE_CALL`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL)). + - `voice-call-uplink`: captures voice call uplink only (mapped to [`VOICE_UPLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)). + - `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)). + - `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)). + ### Duplication An alternative device audio capture method is also available (only for Android diff --git a/doc/build.md b/doc/build.md index 2776ed01..7f76b4fd 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.1`][direct-scrcpy-server] - SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0` + - [`scrcpy-server-v3.3.1`][direct-scrcpy-server] + SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/connection.md b/doc/connection.md index 2c3d37e1..dcf00147 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -113,16 +113,17 @@ with the device IP address you found)_. 7. Run `scrcpy` as usual. 8. Run `adb disconnect` once you're done. -Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass -having to physically connect your device directly to your computer. +Since Android 11, a [wireless debugging option][adb-wireless] allows you to +bypass having to physically connect your device to your computer. [adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line ## Autostart -A small tool (by the scrcpy author) allows to run arbitrary commands whenever a -new Android device is connected: [AutoAdb]. It can be used to start scrcpy: +A small tool (by the scrcpy author) allows you to run arbitrary commands +whenever a new Android device is connected: [AutoAdb]. It can be used to start +scrcpy: ```bash autoadb scrcpy -s '{}' diff --git a/doc/device.md b/doc/device.md index 42208faa..ab1e6ba4 100644 --- a/doc/device.md +++ b/doc/device.md @@ -34,6 +34,31 @@ adb shell settings put global stay_on_while_plugged_in 0 ``` +## Screen off timeout + +The Android screen automatically turns off after some delay. + +To change this delay while scrcpy is running: + +```bash +scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) +``` + +The initial value is restored on exit. + +It is possible to change this setting manually: + +```bash +# get the current screen_off_timeout value +adb shell settings get system screen_off_timeout +# set a new value (in milliseconds) +adb shell settings put system screen_off_timeout 30000 +``` + +Note that the Android value is in milliseconds, but the scrcpy command line +argument is in seconds. + + ## Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -71,31 +96,6 @@ adb shell cmd display power-on 0 ``` -## Screen off timeout - -The Android screen automatically turns off after some delay. - -To change this delay while scrcpy is running: - -```bash -scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) -``` - -The initial value is restored on exit. - -It is possible to change this setting manually: - -```bash -# get the current screen_off_timeout value -adb shell settings get system screen_off_timeout -# set a new value (in milliseconds) -adb shell settings put system screen_off_timeout 30000 -``` - -Note that the Android value is in milliseconds, but the scrcpy command line -argument is in seconds. - - ## Show touches For presentations, it may be useful to show physical touches (on the physical diff --git a/doc/linux.md b/doc/linux.md index 9beaed1e..be433df4 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a` + - [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz and extract it. @@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers: - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` - - Snap: `snap install scrcpy` + - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - … (see [repology](https://repology.org/project/scrcpy/versions)) diff --git a/doc/macos.md b/doc/macos.md index 56d9f168..f6b01c30 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf` + - [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139` - - [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87` + - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz and extract it. diff --git a/doc/mouse.md b/doc/mouse.md index 3607a92c..0bea4aea 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -83,9 +83,9 @@ process like the _adb daemon_). ## Mouse bindings By default, with SDK mouse: - - right-click triggers BACK (or POWER on) - - middle-click triggers HOME - - the 4th click triggers APP_SWITCH + - right-click triggers `BACK` (or `POWER` on) + - middle-click triggers `HOME` + - the 4th click triggers `APP_SWITCH` - the 5th click expands the notification panel The secondary clicks may be forwarded to the device instead by pressing the @@ -121,9 +121,9 @@ Each character must be one of the following: - `+`: forward the click to the device - `-`: ignore the click - - `b`: trigger shortcut BACK (or turn screen on if off) - - `h`: trigger shortcut HOME - - `s`: trigger shortcut APP_SWITCH + - `b`: trigger shortcut `BACK` (or turn screen on if off) + - `h`: trigger shortcut `HOME` + - `s`: trigger shortcut `APP_SWITCH` - `n`: trigger shortcut "expand notification panel" For example: diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5d1673e8..9f962127 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -11,6 +11,8 @@ scrcpy --new-display # use the main display size and density scrcpy --new-display=/240 # use the main display size and 240 dpi ``` +The new virtual display is destroyed on exit. + ## Start app On some devices, a launcher is available in the virtual display. @@ -61,3 +63,15 @@ To move them to the main display instead, use: ``` scrcpy --new-display --no-vd-destroy-content ``` + + +## Display IME policy + +By default, the virtual display IME appears on the default display. + +To make it appear on the local display, use `--display-ime-policy=local`: + +```bash +scrcpy --display-id=1 --display-ime-policy=local +scrcpy --new-display --display-ime-policy=local +``` diff --git a/doc/windows.md b/doc/windows.md index ec7b904b..8fa1921f 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,20 +6,26 @@ Download the [latest release]: - - [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit) - SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca` - - [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit) - SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560` + - [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit) + SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0` + - [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit) + SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-v3.3.1.zip and extract it. ### From a package manager +From [WinGet] (ADB and other dependencies will be installed alongside scrcpy): + +```bash +winget install --exact Genymobile.scrcpy +``` + From [Chocolatey]: ```bash @@ -29,12 +35,12 @@ choco install adb # if you don't have it yet From [Scoop]: - ```bash scoop install scrcpy scoop install adb # if you don't have it yet ``` +[WinGet]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh diff --git a/install_release.sh b/install_release.sh index 3774be86..d960932b 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 -PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 +PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server diff --git a/meson.build b/meson.build index aa1a3a3b..d991d672 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', - version: '3.1', - meson_version: '>= 0.48', + version: '3.3.1', + meson_version: '>= 0.49', default_options: [ 'c_std=c11', 'warning_level=2', diff --git a/server/build.gradle b/server/build.gradle index 9c0543e9..31092b12 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30100 - versionName "3.1" + versionCode 30301 + versionName "3.3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d16592b4..193a9902 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.1 +SCRCPY_VERSION_NAME=3.3.1 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} @@ -47,10 +47,8 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ android/view/IDisplayWindowListener.aidl diff --git a/server/meson.build b/server/meson.build index 42b97981..55828e2d 100644 --- a/server/meson.build +++ b/server/meson.build @@ -23,3 +23,9 @@ else install: true, install_dir: 'share/scrcpy') endif + +if meson.version().version_compare('>= 0.58.0') + devenv = environment() + devenv.set('SCRCPY_SERVER_PATH', meson.current_build_dir() / 'scrcpy-server') + meson.add_devenv(devenv) +endif diff --git a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl deleted file mode 100644 index 2c91149d..00000000 --- a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -/** - * {@hide} - */ -oneway interface IDisplayFoldListener -{ - /** Called when the foldedness of a display changes */ - void onDisplayFoldChanged(int displayId, boolean folded); -} diff --git a/server/src/main/aidl/android/view/IRotationWatcher.aidl b/server/src/main/aidl/android/view/IRotationWatcher.aidl deleted file mode 100644 index 2cc5e44a..00000000 --- a/server/src/main/aidl/android/view/IRotationWatcher.aidl +++ /dev/null @@ -1,25 +0,0 @@ -/* //device/java/android/android/hardware/ISensorListener.aidl -** -** Copyright 2008, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ - -package android.view; - -/** - * {@hide} - */ -interface IRotationWatcher { - oneway void onRotationChanged(int rotation); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 49b23e81..77018afa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -4,8 +4,10 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Settings; 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; @@ -97,18 +99,31 @@ public final class CleanUp { } } - boolean powerOffScreen = options.getPowerOffScreenOnClose(); int displayId = options.getDisplayId(); + int restoreDisplayImePolicy = -1; + if (displayId > 0) { + int displayImePolicy = options.getDisplayImePolicy(); + if (displayImePolicy != -1) { + int currentDisplayImePolicy = ServiceManager.getWindowManager().getDisplayImePolicy(displayId); + if (currentDisplayImePolicy != displayImePolicy) { + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy); + restoreDisplayImePolicy = currentDisplayImePolicy; + } + } + } + + boolean powerOffScreen = options.getPowerOffScreenOnClose(); + try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout, restoreDisplayImePolicy); } catch (IOException e) { Ln.e("Clean up I/O exception", e); } } - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) - throws IOException { + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout, + int restoreDisplayImePolicy) throws IOException { String[] cmd = { "app_process", "/", @@ -118,6 +133,7 @@ public final class CleanUp { String.valueOf(disableShowTouches), String.valueOf(powerOffScreen), String.valueOf(restoreScreenOffTimeout), + String.valueOf(restoreDisplayImePolicy), }; ProcessBuilder builder = new ProcessBuilder(cmd); @@ -164,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 @@ -173,11 +194,15 @@ public final class CleanUp { } unlinkSelf(); + // Needed for workarounds + prepareMainLooper(); + int displayId = Integer.parseInt(args[0]); int restoreStayOn = Integer.parseInt(args[1]); boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean powerOffScreen = Boolean.parseBoolean(args[3]); int restoreScreenOffTimeout = Integer.parseInt(args[4]); + int restoreDisplayImePolicy = Integer.parseInt(args[5]); // Dynamic option boolean restoreDisplayPower = false; @@ -223,6 +248,11 @@ public final class CleanUp { } } + if (restoreDisplayImePolicy != -1) { + Ln.i("Restoring \"display IME policy\""); + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, restoreDisplayImePolicy); + } + // Change the power of the main display when mirroring a virtual display int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; if (Device.isScreenOn(targetDisplayId)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2b83e397..b43e9e1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,8 +2,10 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; @@ -11,6 +13,8 @@ import android.content.IContentProvider; import android.os.Binder; import android.os.Process; +import java.lang.reflect.Field; + public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; @@ -91,4 +95,25 @@ public final class FakeContext extends ContextWrapper { public ContentResolver getContentResolver() { return contentResolver; } + + @SuppressLint("SoonBlockedPrivateApi") + @Override + public Object getSystemService(String name) { + Object service = super.getSystemService(name); + if (service == null) { + return null; + } + + if (Context.CLIPBOARD_SERVICE.equals(name)) { + try { + Field field = ClipboardManager.class.getDeclaredField("mContext"); + field.setAccessible(true); + field.set(service, this); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + return service; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 8a438750..66bb68e8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.video.CameraAspectRatio; import com.genymobile.scrcpy.video.CameraFacing; import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.util.Pair; @@ -48,6 +49,7 @@ public class Options { private boolean showTouches; private boolean stayAwake; private int screenOffTimeout = -1; + private int displayImePolicy = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -186,6 +188,10 @@ public class Options { return screenOffTimeout; } + public int getDisplayImePolicy() { + return displayImePolicy; + } + public List getVideoCodecOptions() { return videoCodecOptions; } @@ -482,6 +488,9 @@ public class Options { options.captureOrientationLock = pair.first; options.captureOrientation = pair.second; break; + case "display_ime_policy": + options.displayImePolicy = parseDisplayImePolicy(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -626,4 +635,17 @@ public class Options { return Pair.create(lock, Orientation.getByName(value)); } + + private static int parseDisplayImePolicy(String value) { + switch (value) { + case "local": + return WindowManager.DISPLAY_IME_POLICY_LOCAL; + case "fallback": + return WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + case "hide": + return WindowManager.DISPLAY_IME_POLICY_HIDE; + default: + throw new IllegalArgumentException("Invalid display IME policy: " + value); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eb8b533a..a08c948c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -24,10 +24,13 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import android.annotation.SuppressLint; import android.os.Build; +import android.os.Looper; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -55,17 +58,7 @@ public final class Server { this.fatalError = true; } if (running == 0 || this.fatalError) { - notify(); - } - } - - synchronized void await() { - try { - while (running > 0 && !fatalError) { - wait(); - } - } catch (InterruptedException e) { - // ignore + Looper.getMainLooper().quitSafely(); } } } @@ -80,9 +73,15 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) { - Ln.e("New virtual display is not supported before Android 10"); - throw new ConfigurationException("New virtual display is not supported"); + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { + if (options.getNewDisplay() != null) { + Ln.e("New virtual display is not supported before Android 10"); + throw new ConfigurationException("New virtual display is not supported"); + } + if (options.getDisplayImePolicy() != -1) { + Ln.e("Display IME policy is not supported before Android 10"); + throw new ConfigurationException("Display IME policy is not supported"); + } } CleanUp cleanUp = null; @@ -166,7 +165,7 @@ public final class Server { }); } - completion.await(); + Looper.loop(); // interrupted by the Completion implementation } finally { if (cleanUp != null) { cleanUp.interrupt(); @@ -195,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 { @@ -215,6 +229,8 @@ public final class Server { Ln.e("Exception on thread " + t, e); }); + prepareMainLooper(); + Options options = Options.parse(args); Ln.disableSystemStreams(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index eec00a04..b89f19ae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -29,8 +29,6 @@ public final class Workarounds { private static final Object ACTIVITY_THREAD; static { - prepareMainLooper(); - try { // ActivityThread activityThread = new ActivityThread(); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); @@ -42,6 +40,11 @@ public final class Workarounds { Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); sCurrentActivityThreadField.setAccessible(true); sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + + // activityThread.mSystemThread = true; + Field mSystemThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("mSystemThread"); + mSystemThreadField.setAccessible(true); + mSystemThreadField.setBoolean(ACTIVITY_THREAD, true); } catch (Exception e) { throw new AssertionError(e); } @@ -72,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(); @@ -132,10 +122,13 @@ public final class Workarounds { try { Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + + // configurationController = new ConfigurationController(ACTIVITY_THREAD); Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); configurationControllerConstructor.setAccessible(true); Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + // ACTIVITY_THREAD.mConfigurationController = configurationController; Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); configurationControllerField.setAccessible(true); configurationControllerField.set(ACTIVITY_THREAD, configurationController); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 5c859738..bf870bee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -12,7 +12,6 @@ import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; import android.media.MediaCodec; -import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -32,18 +31,7 @@ public class AudioDirectCapture implements AudioCapture { private AudioRecordReader reader; public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = getAudioSourceValue(audioSource); - } - - private static int getAudioSourceValue(AudioSource audioSource) { - switch (audioSource) { - case OUTPUT: - return MediaRecorder.AudioSource.REMOTE_SUBMIX; - case MIC: - return MediaRecorder.AudioSource.MIC; - default: - throw new IllegalArgumentException("Unsupported audio source: " + audioSource); - } + this.audioSource = audioSource.getDirectAudioSource(); } @TargetApi(AndroidVersions.API_23_ANDROID_6_0) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 267be60a..33177228 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -55,6 +55,9 @@ public final class AudioEncoder implements AsyncProcessor { private final List codecOptions; private final String encoderName; + private boolean recreatePts; + private long previousPts; + // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); @@ -118,6 +121,9 @@ public final class AudioEncoder implements AsyncProcessor { OutputTask task = outputTasks.take(); ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); try { + if (recreatePts) { + fixTimestamp(task.bufferInfo); + } streamer.writePacket(buffer, task.bufferInfo); } finally { mediaCodec.releaseOutputBuffer(task.index, false); @@ -125,6 +131,25 @@ public final class AudioEncoder implements AsyncProcessor { } } + private void fixTimestamp(MediaCodec.BufferInfo bufferInfo) { + assert recreatePts; + + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // Config packet, nothing to fix + return; + } + + long pts = bufferInfo.presentationTimeUs; + if (previousPts != 0) { + long now = System.nanoTime() / 1000; + // This specific encoder produces PTS matching the exact number of samples + long duration = pts - previousPts; + bufferInfo.presentationTimeUs = now - duration; + } + + previousPts = pts; + } + @Override public void start(TerminationListener listener) { thread = new Thread(() -> { @@ -194,6 +219,12 @@ public final class AudioEncoder implements AsyncProcessor { Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); + // The default OPUS and FLAC encoders overwrite the input PTS with a value that matches the number of samples. This is not the behavior + // we want: it ignores any audio clock drift and hard silences (packets not produced on silence). To work around this behavior, + // regenerate PTS based on the current time and the packet duration. + String codecName = mediaCodec.getCanonicalName(); + recreatePts = "c2.android.opus.encoder".equals(codecName) || "c2.android.flac.encoder".equals(codecName); + mediaCodecThread = new HandlerThread("media-codec"); mediaCodecThread.start(); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 6082f20e..d16b5e38 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,20 +1,38 @@ package com.genymobile.scrcpy.audio; +import android.annotation.SuppressLint; +import android.media.MediaRecorder; + +@SuppressLint("InlinedApi") public enum AudioSource { - OUTPUT("output"), - MIC("mic"), - PLAYBACK("playback"); + OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), + MIC("mic", MediaRecorder.AudioSource.MIC), + PLAYBACK("playback", -1), + MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED), + MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER), + MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION), + MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION), + VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL), + VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_UPLINK), + VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_DOWNLINK), + VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE); private final String name; + private final int directAudioSource; - AudioSource(String name) { + AudioSource(String name, int directAudioSource) { this.name = name; + this.directAudioSource = directAudioSource; } public boolean isDirect() { return this != PLAYBACK; } + public int getDirectAudioSource() { + return directAudioSource; + } + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { 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/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 088be7e7..4f8927ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -120,18 +120,40 @@ public final class LogUtils { } } + private static boolean isCameraBackwardCompatible(CameraCharacteristics characteristics) { + int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); + if (capabilities == null) { + return false; + } + + for (int capability : capabilities) { + if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) { + return true; + } + } + + return false; + } + public static String buildCameraListMessage(boolean includeSizes) { StringBuilder builder = new StringBuilder("List of cameras:"); CameraManager cameraManager = ServiceManager.getCameraManager(); try { String[] cameraIds = cameraManager.getCameraIdList(); - if (cameraIds == null || cameraIds.length == 0) { + if (cameraIds.length == 0) { builder.append("\n (none)"); } else { for (String id : cameraIds) { - builder.append("\n --camera-id=").append(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + if (!isCameraBackwardCompatible(characteristics)) { + // Ignore depth cameras as suggested by official documentation + // + continue; + } + + builder.append("\n --camera-id=").append(id); + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); builder.append(" (").append(getCameraFacingName(facing)).append(", "); @@ -141,8 +163,10 @@ public final class LogUtils { try { // Capture frame rates for low-FPS mode are the same for every resolution Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); - builder.append(", fps=").append(uniqueLowFps); + if (lowFpsRanges != null) { + SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); + builder.append(", fps=").append(uniqueLowFps); + } } catch (Exception e) { // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" Ln.w("Could not get available frame rates for camera " + id, e); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java index ff863aa8..3d7cccfe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -23,7 +23,9 @@ public class DisplaySizeMonitor { // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. - private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14; + // It has been broken again after an Android 15 upgrade: + // So use the default method only before Android 14. + private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14; private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; 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 033d6b9a..792b3a8a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -49,6 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; + private int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -68,6 +69,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.newDisplay = options.getNewDisplay(); assert newDisplay != null; this.maxSize = options.getMaxSize(); + this.displayImePolicy = options.getDisplayImePolicy(); this.crop = options.getCrop(); assert options.getCaptureOrientationLock() != null; this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; @@ -191,6 +193,10 @@ public class NewDisplayCapture extends SurfaceCapture { virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + if (displayImePolicy != -1) { + ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); + } + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 791df0f8..54936122 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,270 +1,43 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; -import android.content.IOnPrimaryClipChangedListener; -import android.os.Build; -import android.os.IInterface; - -import java.lang.reflect.Method; +import android.content.Context; public final class ClipboardManager { - private final IInterface manager; - private Method getPrimaryClipMethod; - private Method setPrimaryClipMethod; - private Method addPrimaryClipChangedListener; - private int getMethodVersion; - private int setMethodVersion; - private int addListenerMethodVersion; + private final android.content.ClipboardManager manager; static ClipboardManager create() { - IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); - if (clipboard == null) { + android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); + if (manager == null) { // Some devices have no clipboard manager // // return null; } - return new ClipboardManager(clipboard); + return new ClipboardManager(manager); } - private ClipboardManager(IInterface manager) { + private ClipboardManager(android.content.ClipboardManager manager) { this.manager = manager; } - private Method getGetPrimaryClipMethod() throws NoSuchMethodException { - if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - return getPrimaryClipMethod; - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - getMethodVersion = 0; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - getMethodVersion = 1; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 5; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); - getMethodVersion = 6; - } - return getPrimaryClipMethod; - } - - private Method getSetPrimaryClipMethod() throws NoSuchMethodException { - if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - return setPrimaryClipMethod; - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - setMethodVersion = 0; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e1) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - setMethodVersion = 1; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e2) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e3) { - // fall-through - } - - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); - setMethodVersion = 3; - } - return setPrimaryClipMethod; - } - - private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); - } - - switch (methodVersion) { - case 0: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - case 1: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - case 2: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - case 3: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); - case 4: - // The last boolean parameter is "userOperate" - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - case 5: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); - default: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); - } - } - - private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - case 2: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - default: - // The last boolean parameter is "userOperate" - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - } - } - public CharSequence getText() { - try { - Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); - if (clipData == null || clipData.getItemCount() == 0) { - return null; - } - return clipData.getItemAt(0).getText(); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + ClipData clipData = manager.getPrimaryClip(); + if (clipData == null || clipData.getItemCount() == 0) { return null; } + return clipData.getItemAt(0).getText(); } public boolean setText(CharSequence text) { - try { - Method method = getSetPrimaryClipMethod(); - ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, setMethodVersion, manager, clipData); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + ClipData clipData = ClipData.newPlainText(null, text); + manager.setPrimaryClip(clipData); + return true; } - private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) - throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - default: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - } - } - - private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { - if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); - } else { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); - addListenerMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class); - addListenerMethodVersion = 1; - } catch (NoSuchMethodException e2) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class, int.class); - addListenerMethodVersion = 2; - } - } - } - } - return addPrimaryClipChangedListener; - } - - public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { - try { - Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { + manager.addPrimaryClipChangedListener(listener); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index d44ac608..a12470a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -46,6 +46,7 @@ public final class DisplayManager { } private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal + private Method getDisplayInfoMethod; private Method createVirtualDisplayMethod; private Method requestDisplayPowerMethod; @@ -81,7 +82,7 @@ public final class DisplayManager { int density = Integer.parseInt(m.group(5)); int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -95,12 +96,12 @@ public final class DisplayManager { } private static int parseDisplayFlags(String text) { - Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); if (text == null) { return 0; } int flags = 0; + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); Matcher m = regex.matcher(text); while (m.find()) { String flagString = m.group(); @@ -114,9 +115,18 @@ public final class DisplayManager { return flags; } + // getDisplayInfo() may be used from both the Controller thread and the video (main) thread + private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException { + if (getDisplayInfoMethod == null) { + getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); + } + return getDisplayInfoMethod; + } + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + Method method = getGetDisplayInfoMethod(); + Object displayInfo = method.invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); @@ -129,7 +139,8 @@ public final class DisplayManager { int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); + String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 5c5ba56c..f55648d5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,11 +1,15 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.view.InputEvent; import android.view.MotionEvent; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -15,39 +19,28 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final Object manager; - private Method injectInputEventMethod; + private final android.hardware.input.InputManager manager; + private long lastPermissionLogDate; + private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; + private static Method addUniqueIdAssociationByPortMethod; + private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { - try { - Class inputManagerClass = getInputManagerClass(); - Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); - Object im = getInstanceMethod.invoke(null); - return new InputManager(im); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } + android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() + .getSystemService(FakeContext.INPUT_SERVICE); + return new InputManager(manager); } - private static Class getInputManagerClass() { - try { - // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview - return Class.forName("android.hardware.input.InputManagerGlobal"); - } catch (ClassNotFoundException e) { - return android.hardware.input.InputManager.class; - } - } - - private InputManager(Object manager) { + private InputManager(android.hardware.input.InputManager manager) { this.manager = manager; } - private Method getInjectInputEventMethod() throws NoSuchMethodException { + private static Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } @@ -57,6 +50,23 @@ public final class InputManager { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); } catch (ReflectiveOperationException e) { + if (e instanceof InvocationTargetException) { + Throwable cause = e.getCause(); + if (cause instanceof SecurityException) { + String message = e.getCause().getMessage(); + if (message != null && message.contains("INJECT_EVENTS permission")) { + // Do not flood the console, limit to one permission error log every 3 seconds + long now = System.currentTimeMillis(); + if (lastPermissionLogDate <= now - 3000) { + Ln.e(message); + Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device."); + lastPermissionLogDate = now; + } + // Do not print the stack trace + return false; + } + } + } Ln.e("Could not invoke method", e); return false; } @@ -97,4 +107,40 @@ public final class InputManager { return false; } } + + private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (addUniqueIdAssociationByPortMethod == null) { + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "addUniqueIdAssociationByPort", String.class, String.class); + } + return addUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) { + try { + Method method = getAddUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort, uniqueId); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot add unique id association by port", e); + } + } + + private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (removeUniqueIdAssociationByPortMethod == null) { + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "removeUniqueIdAssociationByPort", String.class); + } + return removeUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void removeUniqueIdAssociationByPort(String inputPort) { + try { + Method method = getRemoveUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot remove unique id association by port", e); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index a8a56dab..b1123b55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -54,7 +54,8 @@ public final class ServiceManager { return windowManager; } - public static DisplayManager getDisplayManager() { + // The DisplayManager may be used from both the Controller thread and the video (main) thread + public static synchronized DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = DisplayManager.create(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 86dd83f2..7ba5cc06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,14 +4,20 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; +import android.os.Build; import android.os.IInterface; -import android.view.IDisplayFoldListener; import android.view.IDisplayWindowListener; -import android.view.IRotationWatcher; import java.lang.reflect.Method; public final class WindowManager { + + @SuppressWarnings("checkstyle:LineLength") + // + public static final int DISPLAY_IME_POLICY_LOCAL = 0; + public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; + public static final int DISPLAY_IME_POLICY_HIDE = 2; + private final IInterface manager; private Method getRotationMethod; @@ -24,6 +30,9 @@ public final class WindowManager { private Method thawDisplayRotationMethod; private int thawDisplayRotationMethodVersion; + private Method getDisplayImePolicyMethod; + private Method setDisplayImePolicyMethod; + static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); return new WindowManager(manager); @@ -182,52 +191,6 @@ public final class WindowManager { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { - try { - Class cls = manager.getClass(); - try { - // display parameter added since this commit: - // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); - } catch (NoSuchMethodException e) { - // old version - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); - } - } catch (Exception e) { - Ln.e("Could not register rotation watcher", e); - } - } - - public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) { - try { - manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher); - } catch (Exception e) { - Ln.e("Could not unregister rotation watcher", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { - try { - manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); - } catch (Exception e) { - Ln.e("Could not register display fold listener", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) { - try { - manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); - } catch (Exception e) { - Ln.e("Could not unregister display fold listener", e); - } - } - @TargetApi(AndroidVersions.API_30_ANDROID_11) public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { try { @@ -246,4 +209,59 @@ public final class WindowManager { Ln.e("Could not unregister display window listener", e); } } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getGetDisplayImePolicyMethod() throws NoSuchMethodException { + if (getDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + getDisplayImePolicyMethod = manager.getClass().getMethod("getDisplayImePolicy", int.class); + } else { + getDisplayImePolicyMethod = manager.getClass().getMethod("shouldShowIme", int.class); + } + } + return getDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public int getDisplayImePolicy(int displayId) { + try { + Method method = getGetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + return (int) method.invoke(manager, displayId); + } + boolean shouldShowIme = (boolean) method.invoke(manager, displayId); + return shouldShowIme ? DISPLAY_IME_POLICY_LOCAL : DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return -1; + } + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getSetDisplayImePolicyMethod() throws NoSuchMethodException { + if (setDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + setDisplayImePolicyMethod = manager.getClass().getMethod("setDisplayImePolicy", int.class, int.class); + } else { + setDisplayImePolicyMethod = manager.getClass().getMethod("setShouldShowIme", int.class, boolean.class); + } + } + return setDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public void setDisplayImePolicy(int displayId, int displayImePolicy) { + try { + Method method = getSetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + method.invoke(manager, displayId, displayImePolicy); + } else if (displayImePolicy != DISPLAY_IME_POLICY_HIDE) { + method.invoke(manager, displayId, displayImePolicy == DISPLAY_IME_POLICY_LOCAL); + } else { + Ln.w("DISPLAY_IME_POLICY_HIDE is not supported before Android 12"); + } + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + } + } } 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