mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-23 23:04:43 +01:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ee0a52cb | ||
|
|
db2f72a58a | ||
|
|
fb6381f5b9 | ||
|
|
06fd3b4786 | ||
|
|
1d1a4103cc | ||
|
|
5b51396a8c | ||
|
|
7e66062086 | ||
|
|
6f9eb31d52 | ||
|
|
9cfa5b197a | ||
|
|
b08093d1c0 | ||
|
|
7dd9bcaf60 | ||
|
|
3530851071 | ||
|
|
d0047b2110 | ||
|
|
3281fda6ef | ||
|
|
925949d54a | ||
|
|
f3d4fde15b | ||
|
|
eee3f24739 | ||
|
|
3e40b24737 | ||
|
|
9d7a4c88e0 | ||
|
|
e5e58b1b30 | ||
|
|
10a0974f43 | ||
|
|
be21e43be5 | ||
|
|
bfb0872493 | ||
|
|
e11399aff0 | ||
|
|
9d56d26d45 | ||
|
|
f663bbec12 | ||
|
|
2506d1768b | ||
|
|
4ee94cb845 | ||
|
|
afaca80b37 | ||
|
|
8057835a0d | ||
|
|
e47529ab9c | ||
|
|
939c8e7f68 | ||
|
|
eb576c44f8 | ||
|
|
0522d02d40 | ||
|
|
30bfc80f9b | ||
|
|
c3d2ef1b1f | ||
|
|
a79ddc35a7 | ||
|
|
04542a9f58 | ||
|
|
8761dcb7a8 | ||
|
|
f01231dff8 | ||
|
|
5b18ce0d2e | ||
|
|
4841fdd1ef | ||
|
|
fc75319bb2 | ||
|
|
7c8bdccbdc | ||
|
|
9787fe5d26 | ||
|
|
98d30288f7 | ||
|
|
d74cfd5711 | ||
|
|
cd3a5d50b6 | ||
|
|
772f42134a | ||
|
|
38256d8ff9 | ||
|
|
4e1cf13a50 | ||
|
|
696402c68c | ||
|
|
dc169e425e | ||
|
|
13fc75902a | ||
|
|
454beaa757 | ||
|
|
1a9ffb3814 | ||
|
|
ac16be54c8 | ||
|
|
8a02e3c2f5 | ||
|
|
283326b2f6 | ||
|
|
ca4f50c5ef | ||
|
|
7a3fe830d4 | ||
|
|
ee414231ed | ||
|
|
41ed40f5f9 | ||
|
|
d2cc930975 | ||
|
|
52f5d08d1f | ||
|
|
70bfa2cf39 | ||
|
|
38f779d9d3 | ||
|
|
8cd63cb63e | ||
|
|
cc309a2b34 | ||
|
|
91a4a74641 | ||
|
|
48f38c4bb6 | ||
|
|
6875e9aa88 | ||
|
|
c5ed2cfc28 | ||
|
|
1a0d300786 | ||
|
|
d2447b5c19 | ||
|
|
5900e9e39c | ||
|
|
882003f314 |
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
run: release/test_client.sh
|
||||
|
||||
build-linux-x86_64:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
@@ -202,8 +202,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew install meson ninja nasm libiconv zlib automake autoconf \
|
||||
libtool
|
||||
brew install meson nasm libiconv zlib automake autoconf libtool
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -230,7 +229,7 @@ jobs:
|
||||
path: release/work/build-macos-aarch64/dist-tar/
|
||||
|
||||
build-macos-x86_64:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15-intel
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
@@ -245,7 +244,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew install meson ninja nasm libiconv zlib automake
|
||||
run: brew install meson nasm libiconv zlib automake
|
||||
# autoconf and libtool are already installed on macos-13
|
||||
|
||||
- name: Build
|
||||
|
||||
5
FAQ.md
5
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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source for the project. Do not download releases from random websites, even if
|
||||
their name contains `scrcpy`.**
|
||||
|
||||
# scrcpy (v3.2)
|
||||
# scrcpy (v3.3.4)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
|
||||
On some devices (especially Xiaomi), you might get the following error:
|
||||
|
||||
```
|
||||
java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
|
||||
Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
|
||||
```
|
||||
|
||||
In that case, you need to enable [an additional option][control] `USB debugging
|
||||
@@ -207,7 +207,7 @@ work][donate]:
|
||||
|
||||
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work
|
||||
|
||||
## Licence
|
||||
## License
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2025 Romain Vimont
|
||||
|
||||
@@ -205,6 +205,7 @@ _scrcpy() {
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
|--screen-off-timeout \
|
||||
|--tunnel-host \
|
||||
|--tunnel-port \
|
||||
|--v4l2-buffer \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#compdef -N scrcpy -N scrcpy.exe
|
||||
#compdef scrcpy scrcpy.exe
|
||||
#
|
||||
# name: scrcpy
|
||||
# auth: hltdev [hltdev8642@gmail.com]
|
||||
@@ -11,7 +11,7 @@ arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--angle=[Rotate the video content by a custom angle, in degrees]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
|
||||
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||
'--audio-dup=[Duplicate audio]'
|
||||
@@ -35,10 +35,10 @@ arguments=(
|
||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||
{-f,--fullscreen}'[Start in fullscreen]'
|
||||
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
||||
'-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
|
||||
'-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]'
|
||||
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
|
||||
{-h,--help}'[Print the help]'
|
||||
'-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
|
||||
'-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]'
|
||||
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
|
||||
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
|
||||
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
||||
@@ -48,7 +48,7 @@ arguments=(
|
||||
'--list-displays[List displays available on the device]'
|
||||
'--list-encoders[List video and audio encoders available on the device]'
|
||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
|
||||
'-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
|
||||
'--mouse-bind=[Configure bindings of secondary clicks]'
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# This file is intended to be sourced by other scripts, not executed
|
||||
|
||||
process_args() {
|
||||
if [[ $# != 3 ]]
|
||||
then
|
||||
# <host>: win32 or win64
|
||||
# <host>: linux, macos, win32 or win64
|
||||
# <build_type>: native or cross
|
||||
# <link_type>: static or shared
|
||||
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
|
||||
@@ -12,8 +11,8 @@ process_args() {
|
||||
fi
|
||||
|
||||
HOST="$1"
|
||||
BUILD_TYPE="$2" # native or cross
|
||||
LINK_TYPE="$3" # static or shared
|
||||
BUILD_TYPE="$2"
|
||||
LINK_TYPE="$3"
|
||||
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
|
||||
|
||||
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
|
||||
|
||||
VERSION=35.0.2
|
||||
FILENAME=platform-tools_r$VERSION-linux.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-linux
|
||||
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
|
||||
VERSION=36.0.0
|
||||
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip"
|
||||
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8
|
||||
|
||||
PROJECT_DIR="platform-tools-$VERSION-linux"
|
||||
FILENAME="$PROJECT_DIR.zip"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
ZIP_PREFIX=platform-tools
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
|
||||
|
||||
VERSION=35.0.2
|
||||
FILENAME=platform-tools_r$VERSION-darwin.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-darwin
|
||||
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
|
||||
VERSION=36.0.0
|
||||
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-darwin.zip"
|
||||
SHA256SUM=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd
|
||||
|
||||
PROJECT_DIR="platform-tools-$VERSION-darwin"
|
||||
FILENAME="$PROJECT_DIR.zip"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
ZIP_PREFIX=platform-tools
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
|
||||
|
||||
VERSION=35.0.2
|
||||
FILENAME=platform-tools_r$VERSION-win.zip
|
||||
PROJECT_DIR=platform-tools-$VERSION-windows
|
||||
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
|
||||
VERSION=36.0.0
|
||||
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-win.zip"
|
||||
SHA256SUM=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c
|
||||
|
||||
PROJECT_DIR="platform-tools-$VERSION-windows"
|
||||
FILENAME="$PROJECT_DIR.zip"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
cd "$PROJECT_DIR"
|
||||
ZIP_PREFIX=platform-tools
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.5.0
|
||||
FILENAME=dav1d-$VERSION.tar.gz
|
||||
PROJECT_DIR=dav1d-$VERSION
|
||||
URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz"
|
||||
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
|
||||
|
||||
PROJECT_DIR="dav1d-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=7.1.1
|
||||
FILENAME=ffmpeg-$VERSION.tar.xz
|
||||
PROJECT_DIR=ffmpeg-$VERSION
|
||||
URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz"
|
||||
SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1
|
||||
|
||||
PROJECT_DIR="ffmpeg-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.xz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=1.0.28
|
||||
FILENAME=libusb-$VERSION.tar.gz
|
||||
PROJECT_DIR=libusb-$VERSION
|
||||
SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe
|
||||
VERSION=1.0.29
|
||||
URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz"
|
||||
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74
|
||||
|
||||
PROJECT_DIR="libusb-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@@ -16,7 +16,7 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001
|
||||
From: Neal Gompa <neal@gompa.dev>
|
||||
Date: Mon, 10 Feb 2025 05:00:56 -0500
|
||||
Subject: [PATCH] pipewire: Ensure that the correct struct is used for
|
||||
enumeration APIs
|
||||
|
||||
PipeWire now requires the correct struct type is used, otherwise
|
||||
it will fail to compile.
|
||||
|
||||
Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f
|
||||
|
||||
Fixes: https://github.com/libsdl-org/SDL/issues/12224
|
||||
(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6)
|
||||
---
|
||||
src/audio/pipewire/SDL_pipewire.c | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c
|
||||
index 889e05decb..5d1bfc28de 100644
|
||||
--- a/src/audio/pipewire/SDL_pipewire.c
|
||||
+++ b/src/audio/pipewire/SDL_pipewire.c
|
||||
@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info)
|
||||
|
||||
/* Need to parse the parameters to get the sample rate */
|
||||
for (i = 0; i < info->n_params; ++i) {
|
||||
- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL);
|
||||
+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL);
|
||||
}
|
||||
|
||||
hotplug_core_sync(node);
|
||||
--
|
||||
2.49.0
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DEPS_DIR"
|
||||
. common
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=2.32.2
|
||||
FILENAME=SDL-$VERSION.tar.gz
|
||||
PROJECT_DIR=SDL-release-$VERSION
|
||||
SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4
|
||||
VERSION=2.32.8
|
||||
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
|
||||
SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c
|
||||
|
||||
PROJECT_DIR="sdl-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@@ -16,9 +16,9 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch
|
||||
get_file "$URL" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION"
|
||||
mv "SDL-release-$VERSION" "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
||||
@@ -29,7 +29,7 @@ export CXXFLAGS="$CFLAGS"
|
||||
|
||||
if [[ -d "$DIRNAME" ]]
|
||||
then
|
||||
echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
|
||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
||||
cd "$DIRNAME"
|
||||
else
|
||||
mkdir "$DIRNAME"
|
||||
|
||||
@@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "3.2"
|
||||
VALUE "ProductVersion", "3.3.4"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb
|
||||
.B \-S, \-\-turn\-screen\-off
|
||||
Turn the device screen off immediately.
|
||||
|
||||
.TP
|
||||
.B "\-\-screen\-off\-timeout " seconds
|
||||
Set the screen off timeout while scrcpy is running (restore the initial value on exit).
|
||||
|
||||
.TP
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
|
||||
|
||||
@@ -103,14 +103,14 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
|
||||
|
||||
static void
|
||||
show_adb_installation_msg(void) {
|
||||
#ifndef __WINDOWS__
|
||||
#ifndef _WIN32
|
||||
static const struct {
|
||||
const char *binary;
|
||||
const char *command;
|
||||
} pkg_managers[] = {
|
||||
{"apt", "apt install adb"},
|
||||
{"apt-get", "apt-get install adb"},
|
||||
{"brew", "brew cask install android-platform-tools"},
|
||||
{"brew", "brew install --cask android-platform-tools"},
|
||||
{"dnf", "dnf install android-tools"},
|
||||
{"emerge", "emerge dev-util/android-tools"},
|
||||
{"pacman", "pacman -S android-tools"},
|
||||
@@ -331,7 +331,7 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
|
||||
bool
|
||||
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
|
||||
const char *remote, unsigned flags) {
|
||||
#ifdef __WINDOWS__
|
||||
#ifdef _WIN32
|
||||
// Windows will parse the string, so the paths must be quoted
|
||||
// (see sys/win/command.c)
|
||||
local = sc_str_quote(local);
|
||||
@@ -351,7 +351,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
|
||||
|
||||
sc_pid pid = sc_adb_execute(argv, flags);
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
#ifdef _WIN32
|
||||
free((void *) remote);
|
||||
free((void *) local);
|
||||
#endif
|
||||
@@ -362,7 +362,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
|
||||
bool
|
||||
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
|
||||
unsigned flags) {
|
||||
#ifdef __WINDOWS__
|
||||
#ifdef _WIN32
|
||||
// Windows will parse the string, so the local name must be quoted
|
||||
// (see sys/win/command.c)
|
||||
local = sc_str_quote(local);
|
||||
@@ -377,7 +377,7 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
|
||||
|
||||
sc_pid pid = sc_adb_execute(argv, flags);
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
#ifdef _WIN32
|
||||
free((void *) local);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -53,7 +53,7 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
|
||||
}
|
||||
uint16_t id = sc_read16be(&buf[1]);
|
||||
size_t size = sc_read16be(&buf[3]);
|
||||
if (size < len - 5) {
|
||||
if (size > len - 5) {
|
||||
return 0; // not available
|
||||
}
|
||||
uint8_t *data = malloc(size);
|
||||
|
||||
@@ -170,6 +170,7 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
|
||||
}
|
||||
}
|
||||
|
||||
av_frame_unref(display->pending.frame);
|
||||
int r = av_frame_ref(display->pending.frame, frame);
|
||||
if (r) {
|
||||
LOGE("Could not ref frame: %d", r);
|
||||
@@ -181,6 +182,11 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
static bool
|
||||
sc_display_update_texture_internal(struct sc_display *display,
|
||||
const AVFrame *frame);
|
||||
|
||||
static bool
|
||||
sc_display_apply_pending(struct sc_display *display) {
|
||||
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
|
||||
@@ -196,7 +202,8 @@ sc_display_apply_pending(struct sc_display *display) {
|
||||
|
||||
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
|
||||
assert(display->pending.frame);
|
||||
bool ok = sc_display_update_texture(display, display->pending.frame);
|
||||
bool ok = sc_display_update_texture_internal(display,
|
||||
display->pending.frame);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct sc_display {
|
||||
|
||||
struct sc_opengl gl;
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GLContext *gl_context;
|
||||
SDL_GLContext gl_context;
|
||||
#endif
|
||||
|
||||
bool mipmaps;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
|
||||
// 1 byte for wheel motion
|
||||
#define SC_HID_MOUSE_INPUT_SIZE 4
|
||||
// 1 byte for wheel motion, 1 byte for hozizontal scrolling
|
||||
#define SC_HID_MOUSE_INPUT_SIZE 5
|
||||
|
||||
/**
|
||||
* Mouse descriptor from the specification:
|
||||
@@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
|
||||
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
|
||||
0x81, 0x06,
|
||||
|
||||
// Usage Page (Consumer Page)
|
||||
0x05, 0x0C,
|
||||
// Usage(AC Pan)
|
||||
0x0A, 0x38, 0x02,
|
||||
// Logical Minimum (-127)
|
||||
0x15, 0x81,
|
||||
// Logical Maximum (127)
|
||||
0x25, 0x7F,
|
||||
// Report Size (8)
|
||||
0x75, 0x08,
|
||||
// Report Count (1)
|
||||
0x95, 0x01,
|
||||
// Input (Data, Variable, Relative): 1 byte (AC Pan)
|
||||
0x81, 0x06,
|
||||
|
||||
// End Collection
|
||||
0xC0,
|
||||
|
||||
@@ -160,7 +175,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
|
||||
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
|
||||
data[1] = CLAMP(event->xrel, -127, 127);
|
||||
data[2] = CLAMP(event->yrel, -127, 127);
|
||||
data[3] = 0; // wheel coordinates only used for scrolling
|
||||
data[3] = 0; // no vertical scrolling
|
||||
data[4] = 0; // no horizontal scrolling
|
||||
}
|
||||
|
||||
void
|
||||
@@ -172,22 +188,27 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
|
||||
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
|
||||
data[1] = 0; // no x motion
|
||||
data[2] = 0; // no y motion
|
||||
data[3] = 0; // wheel coordinates only used for scrolling
|
||||
data[3] = 0; // no vertical scrolling
|
||||
data[4] = 0; // no horizontal scrolling
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
|
||||
const struct sc_mouse_scroll_event *event) {
|
||||
if (!event->vscroll_int && !event->hscroll_int) {
|
||||
// Need a full integral value for HID
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_hid_mouse_input_init(hid_input);
|
||||
|
||||
uint8_t *data = hid_input->data;
|
||||
data[0] = 0; // buttons state irrelevant (and unknown)
|
||||
data[1] = 0; // no x motion
|
||||
data[2] = 0; // no y motion
|
||||
// In practice, vscroll is always -1, 0 or 1, but in theory other values
|
||||
// are possible
|
||||
data[3] = CLAMP(event->vscroll, -127, 127);
|
||||
// Horizontal scrolling ignored
|
||||
data[3] = CLAMP(event->vscroll_int, -127, 127);
|
||||
data[4] = CLAMP(event->hscroll_int, -127, 127);
|
||||
return true;
|
||||
}
|
||||
|
||||
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
|
||||
|
||||
@@ -22,7 +22,7 @@ void
|
||||
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
|
||||
const struct sc_mouse_click_event *event);
|
||||
|
||||
void
|
||||
bool
|
||||
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
|
||||
const struct sc_mouse_scroll_event *event);
|
||||
|
||||
|
||||
@@ -393,6 +393,8 @@ struct sc_mouse_scroll_event {
|
||||
struct sc_position position;
|
||||
float hscroll;
|
||||
float vscroll;
|
||||
int32_t hscroll_int;
|
||||
int32_t vscroll_int;
|
||||
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
|
||||
};
|
||||
|
||||
|
||||
@@ -897,12 +897,14 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
struct sc_mouse_scroll_event evt = {
|
||||
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
|
||||
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
|
||||
.hscroll = event->preciseX,
|
||||
.vscroll = event->preciseY,
|
||||
#else
|
||||
.hscroll = CLAMP(event->x, -1, 1),
|
||||
.vscroll = CLAMP(event->y, -1, 1),
|
||||
.hscroll = event->x,
|
||||
.vscroll = event->y,
|
||||
#endif
|
||||
.hscroll_int = event->x,
|
||||
.vscroll_int = event->y,
|
||||
.buttons_state = im->mouse_buttons_state,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#ifdef HAVE_V4L2
|
||||
# include <libavdevice/avdevice.h>
|
||||
#endif
|
||||
|
||||
@@ -93,7 +93,7 @@ struct scrcpy {
|
||||
|
||||
#ifdef _WIN32
|
||||
static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
|
||||
if (ctrl_type == CTRL_C_EVENT) {
|
||||
if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT) {
|
||||
sc_push_event(SDL_QUIT);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -107,6 +107,17 @@ sdl_set_hints(const char *render_driver) {
|
||||
LOGW("Could not set render driver");
|
||||
}
|
||||
|
||||
// App name used in various contexts (such as PulseAudio)
|
||||
#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME)
|
||||
if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) {
|
||||
LOGW("Could not set app name");
|
||||
}
|
||||
#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME)
|
||||
if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) {
|
||||
LOGW("Could not set audio device app name");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Linear filtering
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
|
||||
LOGW("Could not enable linear filtering");
|
||||
@@ -165,7 +176,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
|
||||
}
|
||||
|
||||
static enum scrcpy_exit_code
|
||||
event_loop(struct scrcpy *s) {
|
||||
event_loop(struct scrcpy *s, bool has_screen) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
@@ -197,7 +208,7 @@ event_loop(struct scrcpy *s) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
||||
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
break;
|
||||
@@ -933,7 +944,7 @@ aoa_complete:
|
||||
}
|
||||
}
|
||||
|
||||
ret = event_loop(s);
|
||||
ret = event_loop(s, options->window);
|
||||
terminate_event_loop();
|
||||
LOGD("quit...");
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ sc_screen_render_novideo(struct sc_screen *screen) {
|
||||
(void) res; // any error already logged
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
||||
#endif
|
||||
|
||||
@@ -409,7 +409,7 @@ sc_screen_init(struct sc_screen *screen,
|
||||
} else {
|
||||
// without video, the icon is used as window content, it must be present
|
||||
LOGE("Could not load icon");
|
||||
goto error_destroy_fps_counter;
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
SDL_Surface *icon_novideo = params->video ? NULL : icon;
|
||||
|
||||
@@ -55,7 +55,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
|
||||
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
|
||||
|
||||
struct sc_hid_input hid_input;
|
||||
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
|
||||
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
|
||||
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
|
||||
|
||||
struct sc_hid_input hid_input;
|
||||
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
|
||||
if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
|
||||
LOGW("Could not push AOA HID input (mouse scroll)");
|
||||
|
||||
@@ -164,8 +164,15 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
|
||||
|
||||
struct sc_mouse_scroll_event evt = {
|
||||
// .position not used for HID events
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
.hscroll = event->preciseX,
|
||||
.vscroll = event->preciseY,
|
||||
#else
|
||||
.hscroll = event->x,
|
||||
.vscroll = event->y,
|
||||
#endif
|
||||
.hscroll_int = event->x,
|
||||
.vscroll_int = event->y,
|
||||
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
# include <ws2tcpip.h>
|
||||
|
||||
@@ -191,7 +191,8 @@ sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size,
|
||||
|
||||
size_t right_len = MIN(size, oldcap - oldorigin);
|
||||
assert(right_len);
|
||||
memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size);
|
||||
memcpy(newptr, (char *) ptr + (oldorigin * item_size),
|
||||
right_len * item_size);
|
||||
|
||||
if (size > right_len) {
|
||||
memcpy((char *) newptr + (right_len * item_size), ptr,
|
||||
|
||||
@@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) {
|
||||
.height = 1920,
|
||||
},
|
||||
},
|
||||
.hscroll = 1,
|
||||
.vscroll = -1,
|
||||
.hscroll = 16,
|
||||
.vscroll = -16,
|
||||
.buttons = 1,
|
||||
},
|
||||
};
|
||||
@@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
|
||||
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
|
||||
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
||||
0x7F, 0xFF, // 1 (float encoded as i16)
|
||||
0x80, 0x00, // -1 (float encoded as i16)
|
||||
0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16])
|
||||
0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16])
|
||||
0x00, 0x00, 0x00, 0x01, // 1
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
@@ -411,6 +411,26 @@ static void test_serialize_open_hard_keyboard(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_start_app(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_START_APP,
|
||||
.start_app = {
|
||||
.name = "firefox",
|
||||
},
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 9);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_START_APP,
|
||||
7, // length
|
||||
'f', 'i', 'r', 'e', 'f', 'o', 'x', // app name
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_reset_video(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
@@ -448,6 +468,7 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_uhid_input();
|
||||
test_serialize_uhid_destroy();
|
||||
test_serialize_open_hard_keyboard();
|
||||
test_serialize_start_app();
|
||||
test_serialize_reset_video();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.1'
|
||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v3.2`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0`</sub>
|
||||
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
@@ -271,7 +271,7 @@ This installs several files:
|
||||
- `/usr/local/bin/scrcpy` (main app)
|
||||
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
|
||||
- `/usr/local/share/man/man1/scrcpy.1` (manpage)
|
||||
- `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon)
|
||||
- `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon)
|
||||
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
|
||||
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be`</sub>
|
||||
- [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
13
doc/macos.md
13
doc/macos.md
@@ -6,15 +6,14 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b`</sub>
|
||||
|
||||
- [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e`</sub>
|
||||
- [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd`</sub>
|
||||
- [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-aarch64-v3.3.4.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-x86_64-v3.3.4.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0`</sub>
|
||||
- [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357`</sub>
|
||||
- [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`</sub>
|
||||
- [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win64-v3.3.4.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
# https://gradle.org/release-checksums/
|
||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
||||
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
|
||||
PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
|
||||
PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '3.2',
|
||||
version: '3.3.4',
|
||||
meson_version: '>= 0.49',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace 'com.genymobile.scrcpy'
|
||||
compileSdk 35
|
||||
namespace = 'com.genymobile.scrcpy'
|
||||
compileSdk 36
|
||||
defaultConfig {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
applicationId = "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode 30200
|
||||
versionName "3.2"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
targetSdkVersion 36
|
||||
versionCode 30304
|
||||
versionName "3.3.4"
|
||||
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -18,8 +18,11 @@ android {
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
aidl true
|
||||
buildConfig = true
|
||||
aidl = true
|
||||
}
|
||||
lint {
|
||||
disable 'UseRequiresApi'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=3.2
|
||||
SCRCPY_VERSION_NAME=3.3.4
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-35}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
|
||||
PLATFORM=${ANDROID_PLATFORM:-36}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
|
||||
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
|
||||
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||
|
||||
@@ -86,7 +86,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
|
||||
echo "Dexing..."
|
||||
cd "$CLASSES_DIR"
|
||||
|
||||
if [[ $PLATFORM -lt 31 ]]
|
||||
if [[ "${PLATFORM%%.*}" -lt 31 ]]
|
||||
then
|
||||
# use dx
|
||||
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
|
||||
|
||||
@@ -48,19 +48,4 @@ oneway interface IDisplayWindowListener {
|
||||
* Called when a display is removed from the hierarchy.
|
||||
*/
|
||||
void onDisplayRemoved(int displayId);
|
||||
|
||||
/**
|
||||
* Called when fixed rotation is started on a display.
|
||||
*/
|
||||
void onFixedRotationStarted(int displayId, int newRotation);
|
||||
|
||||
/**
|
||||
* Called when the previous fixed rotation on a display is finished.
|
||||
*/
|
||||
void onFixedRotationFinished(int displayId);
|
||||
|
||||
/**
|
||||
* Called when the keep clear ares on a display have changed.
|
||||
*/
|
||||
void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.genymobile.scrcpy.util.SettingsException;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Looper;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
|
||||
@@ -179,6 +180,11 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void prepareMainLooper() {
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
try {
|
||||
// Start a new session to avoid being terminated along with the server process on some devices
|
||||
@@ -188,6 +194,10 @@ public final class CleanUp {
|
||||
}
|
||||
unlinkSelf();
|
||||
|
||||
// Needed for workarounds
|
||||
prepareMainLooper();
|
||||
Workarounds.apply();
|
||||
|
||||
int displayId = Integer.parseInt(args[0]);
|
||||
int restoreStayOn = Integer.parseInt(args[1]);
|
||||
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.ContentResolver;
|
||||
@@ -11,6 +12,8 @@ import android.content.IContentProvider;
|
||||
import android.os.Binder;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.android.shell";
|
||||
@@ -72,7 +75,7 @@ public final class FakeContext extends ContextWrapper {
|
||||
@Override
|
||||
public AttributionSource getAttributionSource() {
|
||||
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
|
||||
builder.setPackageName("shell");
|
||||
builder.setPackageName(PACKAGE_NAME);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -87,8 +90,38 @@ public final class FakeContext extends ContextWrapper {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context createPackageContext(String packageName, int flags) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentResolver getContentResolver() {
|
||||
return contentResolver;
|
||||
}
|
||||
|
||||
@SuppressLint("SoonBlockedPrivateApi")
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
Object service = super.getSystemService(name);
|
||||
if (service == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// "semclipboard" is a Samsung-internal service
|
||||
// See:
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/6224>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/6523>
|
||||
if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name) || Context.ACTIVITY_SERVICE.equals(name)) {
|
||||
try {
|
||||
Field field = service.getClass().getDeclaredField("mContext");
|
||||
field.setAccessible(true);
|
||||
field.set(service, this);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,13 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -55,17 +58,7 @@ public final class Server {
|
||||
this.fatalError = true;
|
||||
}
|
||||
if (running == 0 || this.fatalError) {
|
||||
notify();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void await() {
|
||||
try {
|
||||
while (running > 0 && !fatalError) {
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
Looper.getMainLooper().quitSafely();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +165,7 @@ public final class Server {
|
||||
});
|
||||
}
|
||||
|
||||
completion.await();
|
||||
Looper.loop(); // interrupted by the Completion implementation
|
||||
} finally {
|
||||
if (cleanUp != null) {
|
||||
cleanUp.interrupt();
|
||||
@@ -201,6 +194,21 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareMainLooper() {
|
||||
// Like Looper.prepareMainLooper(), but with quitAllowed set to true
|
||||
Looper.prepare();
|
||||
synchronized (Looper.class) {
|
||||
try {
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
Field field = Looper.class.getDeclaredField("sMainLooper");
|
||||
field.setAccessible(true);
|
||||
field.set(null, Looper.myLooper());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) {
|
||||
int status = 0;
|
||||
try {
|
||||
@@ -217,10 +225,16 @@ public final class Server {
|
||||
}
|
||||
|
||||
private static void internalMain(String... args) throws Exception {
|
||||
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
if (defaultHandler != null) {
|
||||
defaultHandler.uncaughtException(t, e);
|
||||
}
|
||||
});
|
||||
|
||||
prepareMainLooper();
|
||||
|
||||
Options options = Options.parse(args);
|
||||
|
||||
Ln.disableSystemStreams();
|
||||
|
||||
@@ -6,9 +6,9 @@ import com.genymobile.scrcpy.util.Ln;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
@@ -29,8 +29,6 @@ public final class Workarounds {
|
||||
private static final Object ACTIVITY_THREAD;
|
||||
|
||||
static {
|
||||
prepareMainLooper();
|
||||
|
||||
try {
|
||||
// ActivityThread activityThread = new ActivityThread();
|
||||
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
|
||||
@@ -77,19 +75,6 @@ public final class Workarounds {
|
||||
fillAppContext();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void prepareMainLooper() {
|
||||
// Some devices internally create a Handler when creating an input Surface, causing an exception:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
//
|
||||
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
|
||||
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
|
||||
// on a null object reference"
|
||||
// <https://github.com/Genymobile/scrcpy/issues/921>
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
private static void fillAppInfo() {
|
||||
try {
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
@@ -118,10 +103,7 @@ public final class Workarounds {
|
||||
|
||||
private static void fillAppContext() {
|
||||
try {
|
||||
Application app = new Application();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
baseField.setAccessible(true);
|
||||
baseField.set(app, FakeContext.get());
|
||||
Application app = Instrumentation.newApplication(Application.class, FakeContext.get());
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Integer, FileDescriptor> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.genymobile.scrcpy.opengl;
|
||||
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Threads;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.EGL14;
|
||||
@@ -15,6 +16,7 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public final class OpenGLRunner {
|
||||
@@ -80,31 +82,17 @@ public final class OpenGLRunner {
|
||||
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
|
||||
initOnce();
|
||||
|
||||
// Simulate CompletableFuture, but working for all Android versions
|
||||
final Semaphore sem = new Semaphore(0);
|
||||
Throwable[] throwableRef = new Throwable[1];
|
||||
|
||||
// The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
|
||||
// See <https://github.com/Genymobile/scrcpy/issues/5444>
|
||||
handler.post(() -> {
|
||||
try {
|
||||
run(inputSize, outputSize, outputSurface);
|
||||
} catch (Throwable throwable) {
|
||||
throwableRef[0] = throwable;
|
||||
} finally {
|
||||
sem.release();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
sem.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
// Behave as if this method call was synchronous
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
Throwable throwable = throwableRef[0];
|
||||
if (throwable != null) {
|
||||
Threads.executeSynchronouslyOn(handler, new Callable<Void>() {
|
||||
@Override
|
||||
public Void call() throws Exception {
|
||||
run(inputSize, outputSize, outputSurface);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
if (throwable instanceof OpenGLException) {
|
||||
throw (OpenGLException) throwable;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@ public final class Ln {
|
||||
public static void w(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.WARN)) {
|
||||
Log.w(TAG, message, throwable);
|
||||
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
synchronized (CONSOLE_ERR) {
|
||||
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +90,11 @@ public final class Ln {
|
||||
public static void e(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.ERROR)) {
|
||||
Log.e(TAG, message, throwable);
|
||||
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
synchronized (CONSOLE_ERR) {
|
||||
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace(CONSOLE_ERR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import com.genymobile.scrcpy.AndroidVersions;
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class Settings {
|
||||
|
||||
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
|
||||
@@ -18,66 +13,26 @@ public final class Settings {
|
||||
/* not instantiable */
|
||||
}
|
||||
|
||||
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
|
||||
try {
|
||||
Command.exec("settings", "put", table, key, value);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new SettingsException("put", table, key, value, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String execSettingsGet(String table, String key) throws SettingsException {
|
||||
try {
|
||||
return Command.execReadLine("settings", "get", table, key);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new SettingsException("get", table, key, null, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getValue(String table, String key) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||
return provider.getValue(table, key);
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||
return provider.getValue(table, key);
|
||||
}
|
||||
|
||||
return execSettingsGet(table, key);
|
||||
}
|
||||
|
||||
public static void putValue(String table, String key, String value) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||
provider.putValue(table, key, value);
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||
provider.putValue(table, key, value);
|
||||
}
|
||||
|
||||
execSettingsPut(table, key, value);
|
||||
}
|
||||
|
||||
public static String getAndPutValue(String table, String key, String value) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||
String oldValue = provider.getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
provider.putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
|
||||
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
|
||||
String oldValue = provider.getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
provider.putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
String oldValue = getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
43
server/src/main/java/com/genymobile/scrcpy/util/Threads.java
Normal file
43
server/src/main/java/com/genymobile/scrcpy/util/Threads.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public final class Threads {
|
||||
private Threads() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static <T> T executeSynchronouslyOn(Handler handler, Callable<T> callable) throws Throwable {
|
||||
// Simulate CompletableFuture, but working for all Android versions
|
||||
final Semaphore sem = new Semaphore(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
T[] resultRef = (T[]) new Object[1];
|
||||
Throwable[] throwableRef = new Throwable[1];
|
||||
|
||||
handler.post(() -> {
|
||||
try {
|
||||
resultRef[0] = callable.call();
|
||||
} catch (Throwable throwable) {
|
||||
throwableRef[0] = throwable;
|
||||
} finally {
|
||||
sem.release();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
sem.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
// Behave as if this method call was synchronous
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
if (throwableRef[0] != null) {
|
||||
throw throwableRef[0];
|
||||
}
|
||||
|
||||
return resultRef[0];
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
|
||||
// Internal fields copied from android.hardware.display.DisplayManager
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_PRESENTATION = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
|
||||
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
|
||||
@@ -169,6 +170,7 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
int virtualDisplayId;
|
||||
try {
|
||||
int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC
|
||||
| VIRTUAL_DISPLAY_FLAG_PRESENTATION
|
||||
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
|
||||
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
|
||||
|
||||
@@ -129,17 +129,26 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
.createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface);
|
||||
Ln.d("Display: using DisplayManager API");
|
||||
} catch (Exception displayManagerException) {
|
||||
try {
|
||||
display = createDisplay();
|
||||
if (Build.BRAND.equalsIgnoreCase("oculus") && Build.MODEL.equalsIgnoreCase("Quest 3")) {
|
||||
// Workaround for buggy createVirtualDisplay on Quest 3
|
||||
try {
|
||||
virtualDisplay = (VirtualDisplay) VirtualDisplay.class.getDeclaredConstructors()[0].newInstance(null, null, null, surface);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not create VirtualDisplay", e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
display = createDisplay();
|
||||
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
int layerStack = displayInfo.getLayerStack();
|
||||
setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack);
|
||||
Ln.d("Display: using SurfaceControl API");
|
||||
} catch (Exception surfaceControlException) {
|
||||
Ln.e("Could not create display using DisplayManager", displayManagerException);
|
||||
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
|
||||
throw new AssertionError("Could not create display");
|
||||
Size deviceSize = displayInfo.getSize();
|
||||
int layerStack = displayInfo.getLayerStack();
|
||||
setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack);
|
||||
Ln.d("Display: using SurfaceControl API");
|
||||
} catch (Exception surfaceControlException) {
|
||||
Ln.e("Could not create display using DisplayManager", displayManagerException);
|
||||
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
|
||||
throw new AssertionError("Could not create display");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public final class DisplayManager {
|
||||
}
|
||||
|
||||
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
|
||||
private Method getDisplayInfoMethod;
|
||||
private Method createVirtualDisplayMethod;
|
||||
private Method requestDisplayPowerMethod;
|
||||
|
||||
@@ -81,7 +82,7 @@ public final class DisplayManager {
|
||||
int density = Integer.parseInt(m.group(5));
|
||||
int layerStack = Integer.parseInt(m.group(6));
|
||||
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null);
|
||||
}
|
||||
|
||||
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
|
||||
@@ -95,12 +96,12 @@ public final class DisplayManager {
|
||||
}
|
||||
|
||||
private static int parseDisplayFlags(String text) {
|
||||
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
|
||||
if (text == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
|
||||
Matcher m = regex.matcher(text);
|
||||
while (m.find()) {
|
||||
String flagString = m.group();
|
||||
@@ -114,9 +115,18 @@ public final class DisplayManager {
|
||||
return flags;
|
||||
}
|
||||
|
||||
// getDisplayInfo() may be used from both the Controller thread and the video (main) thread
|
||||
private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException {
|
||||
if (getDisplayInfoMethod == null) {
|
||||
getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class);
|
||||
}
|
||||
return getDisplayInfoMethod;
|
||||
}
|
||||
|
||||
public DisplayInfo getDisplayInfo(int displayId) {
|
||||
try {
|
||||
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
|
||||
Method method = getGetDisplayInfoMethod();
|
||||
Object displayInfo = method.invoke(manager, displayId);
|
||||
if (displayInfo == null) {
|
||||
// fallback when displayInfo is null
|
||||
return getDisplayInfoFromDumpsysDisplay(displayId);
|
||||
@@ -129,7 +139,14 @@ public final class DisplayManager {
|
||||
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi);
|
||||
String uniqueId;
|
||||
try {
|
||||
uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// This field might not exist: <https://github.com/Genymobile/scrcpy/issues/6461>
|
||||
uniqueId = null;
|
||||
}
|
||||
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.view.IDisplayWindowListener;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import java.util.List;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Parcel;
|
||||
import android.os.RemoteException;
|
||||
import android.view.IDisplayWindowListener;
|
||||
|
||||
public class DisplayWindowListener extends IDisplayWindowListener.Stub {
|
||||
@Override
|
||||
@@ -23,17 +24,14 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFixedRotationStarted(int displayId, int newRotation) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFixedRotationFinished(int displayId) {
|
||||
// empty default implementation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) {
|
||||
// empty default implementation
|
||||
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
|
||||
try {
|
||||
return super.onTransact(code, data, reply, flags);
|
||||
} catch (AbstractMethodError e) {
|
||||
Ln.v("Ignoring AbstractMethodError: " + e.getMessage());
|
||||
// Ignore unknown methods, write default response to reply parcel
|
||||
reply.writeNoException();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user