Compare commits

..

15 Commits

Author SHA1 Message Date
Romain Vimont
a5844e198e Add GitHub Actions release workflow
Fixes #4490 <https://github.com/Genymobile/scrcpy/issues/4490>
PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:31:24 +01:00
Romain Vimont
2687d20280 Rework release.mk for CI
Make it possible to build scrcpy-server and Windows binaries in
parallel from different GitHub Actions workflows, and to package
everything as a final step.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:29:13 +01:00
Romain Vimont
9c0a328498 Build server without meson in release.mk
This avoids to install meson/ninja to build scrcpy-server on the CI.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:38 +01:00
Romain Vimont
02ef3d57ce Split client and server tests in release.mk
This will allow to run server tests separately on the CI.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:26 +01:00
Romain Vimont
538a32a539 Fix .PHONY in release.mk
The prepare-deps recipe does not exist anymore. It has been split into
prepare-deps-win32 and prepare-deps-win64.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:13 +01:00
Romain Vimont
9578aae34e Use explicit constant for @TargetApi 2024-10-22 19:47:56 +02:00
Romain Vimont
7b3dd595b4 Remove useless version check
Scrcpy requires Android 5.0+, so there is no point testing for older
versions.

Btw, there were two mistakes:
 - the constant name in AndroidVersions should have been
   API_20_ANDROID_4_4W (Android 4.4 without 'W' is API 19)
 - the method isInteractive() was introduced in Android 5.0, not 4.4W:
   <037c33eae7%5E%21/>
2024-10-21 18:51:35 +02:00
Romain Vimont
a46150f753 Upgrade Android SDK to 35
Also adapt the call to build-tools/35.0.0/aidl, which now requires an
import path (-I. for the current directory).

Otherwise, it fails with:

    ERROR: android/view/IRotationWatcher.aidl:23.1-10: directory ./ is not found in any of the import paths:
     -

Also upgrade AGP (8.7.1) and Gradle (8.9), required for SDK 35.

<https://developer.android.com/build/releases/gradle-plugin#updating-gradle>
2024-10-20 15:26:19 +02:00
Romain Vimont
3acffaae57 Use explicit constants for Android versions
Who remembers code names? This avoids to check the mapping every time.
2024-10-20 13:15:22 +02:00
dillonfrederica
e33be3d288 Fix SDL_events.h include
All SDL includes must be prefixed by "SDL2/".

Fixed #5372 <https://github.com/Genymobile/scrcpy/issues/5372>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-10-12 15:57:24 +02:00
Romain Vimont
c15df01171 Reject non-positive camera sizes early
Throw an exception on parsing if the camera size dimensions are not both
positive.
2024-10-11 22:51:15 +02:00
Romain Vimont
09741bc805 Do not duplicate server string params
The server params were passed from the main thread to the server thread,
so a deep copy was performed in case the caller instance was destroyed.

But in practice, it only contains memory that lives until the end of the
program (command line arguments), so simply reference it. Several copies
of string fields were missing anyway.
2024-10-11 22:42:39 +02:00
Romain Vimont
afbaf59abb Cast to sc_tick type in conversion macros
With the old macros definitions, the type of the result depended on the
type of `sec`.

In particular, if sec is a 32-bit type, sec * 1000000 was likely to
overflow (even if the result was assigned to a sc_tick by the caller of
the macro).

This was the case on Windows, where the long type is a 32-bit signed
integer: the --time-limit argument, expressed in seconds, was first
parsed to a long value, then multiplied by 1000000 by the
SC_TICK_FROM_SEC() macro, causing an overflow when the value was greater
than 2147 (2^31 / 1000000).

Fixes #5355 <https://github.com/Genymobile/scrcpy/issues/5355>
2024-10-08 18:18:05 +02:00
Romain Vimont
5b10650f22 Fix time-limit early interruption
If a value for --time-limit was set, then the thread was not interrupted
on stop (the condvar was not signaled).
2024-10-08 18:12:55 +02:00
Yan
0d8014be52 Fix build error on macOS
Fix window access typo for macOS.

PR #5348 <https://github.com/Genymobile/scrcpy/pull/5348>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-10-07 16:54:15 +02:00
39 changed files with 497 additions and 373 deletions

147
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,147 @@
name: Build
on:
workflow_dispatch:
inputs:
name:
description: 'Version name (default is ref name)'
jobs:
build-scrcpy-server:
runs-on: ubuntu-latest
env:
GRADLE: gradle # use native gradle instead of ./gradlew in release.mk
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Test scrcpy-server
run: make -f release.mk test-server
- name: Build scrcpy-server
run: make -f release.mk build-server
- name: Upload scrcpy-server artifact
uses: actions/upload-artifact@v4
with:
name: scrcpy-server
path: build-server/server/scrcpy-server
test-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
- name: Build
run: |
meson setup d -Db_sanitize=address,undefined
- name: Test
run: |
meson test -Cd
build-win32:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
- name: Build scrcpy win32
run: make -f release.mk build-win32
- name: Upload build-win32 artifact
uses: actions/upload-artifact@v4
with:
name: build-win32-intermediate
path: build-win32/dist/
build-win64:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
- name: Build scrcpy win64
run: make -f release.mk build-win64
- name: Upload build-win64 artifact
uses: actions/upload-artifact@v4
with:
name: build-win64-intermediate
path: build-win64/dist/
package:
needs:
- build-scrcpy-server
- build-win32
- build-win64
runs-on: ubuntu-latest
env:
# $VERSION is used by release.mk
VERSION: ${{ github.event.inputs.name || github.ref_name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: build-server/server/
- name: Download build-win32
uses: actions/download-artifact@v4
with:
name: build-win32-intermediate
path: build-win32/dist/
- name: Download build-win64
uses: actions/download-artifact@v4
with:
name: build-win64-intermediate
path: build-win64/dist/
- name: Package
run: make -f release.mk package
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: scrcpy-release-${{ env.VERSION }}
path: release-${{ env.VERSION }}

View File

@@ -5,7 +5,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL_events.h>
#include <SDL2/SDL_events.h>
enum {
SC_EVENT_NEW_FRAME = SDL_USEREVENT,

View File

@@ -92,8 +92,8 @@ sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(window, &x, &y);
SDL_GetWindowSize(window, &w, &h);
SDL_GetWindowPosition(mc->window, &x, &y);
SDL_GetWindowSize(mc->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;

View File

@@ -66,56 +66,6 @@ get_server_path(void) {
return server_path;
}
static void
sc_server_params_destroy(struct sc_server_params *params) {
// The server stores a copy of the params provided by the user
free((char *) params->req_serial);
free((char *) params->crop);
free((char *) params->video_codec_options);
free((char *) params->audio_codec_options);
free((char *) params->video_encoder);
free((char *) params->audio_encoder);
free((char *) params->tcpip_dst);
free((char *) params->camera_id);
free((char *) params->camera_ar);
}
static bool
sc_server_params_copy(struct sc_server_params *dst,
const struct sc_server_params *src) {
*dst = *src;
// The params reference user-allocated memory, so we must copy them to
// handle them from another thread
#define COPY(FIELD) do { \
dst->FIELD = NULL; \
if (src->FIELD) { \
dst->FIELD = strdup(src->FIELD); \
if (!dst->FIELD) { \
goto error; \
} \
} \
} while(0)
COPY(req_serial);
COPY(crop);
COPY(video_codec_options);
COPY(audio_codec_options);
COPY(video_encoder);
COPY(audio_encoder);
COPY(tcpip_dst);
COPY(camera_id);
COPY(camera_ar);
#undef COPY
return true;
error:
sc_server_params_destroy(dst);
return false;
}
static bool
push_server(struct sc_intr *intr, const char *serial) {
char *server_path = get_server_path();
@@ -499,22 +449,18 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay,
bool
sc_server_init(struct sc_server *server, const struct sc_server_params *params,
const struct sc_server_callbacks *cbs, void *cbs_userdata) {
bool ok = sc_server_params_copy(&server->params, params);
if (!ok) {
LOG_OOM();
return false;
}
// The allocated data in params (const char *) must remain valid until the
// end of the program
server->params = *params;
ok = sc_mutex_init(&server->mutex);
bool ok = sc_mutex_init(&server->mutex);
if (!ok) {
sc_server_params_destroy(&server->params);
return false;
}
ok = sc_cond_init(&server->cond_stopped);
if (!ok) {
sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
return false;
}
@@ -522,7 +468,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
if (!ok) {
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
return false;
}
@@ -1161,7 +1106,6 @@ sc_server_destroy(struct sc_server *server) {
free(server->serial);
free(server->device_socket_name);
sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);

View File

@@ -10,14 +10,14 @@ typedef int64_t sc_tick;
#define SC_TICK_FREQ 1000000 // microsecond
// To be adapted if SC_TICK_FREQ changes
#define SC_TICK_TO_NS(tick) ((tick) * 1000)
#define SC_TICK_TO_US(tick) (tick)
#define SC_TICK_TO_MS(tick) ((tick) / 1000)
#define SC_TICK_TO_SEC(tick) ((tick) / 1000000)
#define SC_TICK_FROM_NS(ns) ((ns) / 1000)
#define SC_TICK_FROM_US(us) (us)
#define SC_TICK_FROM_MS(ms) ((ms) * 1000)
#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000)
#define SC_TICK_TO_NS(tick) ((sc_tick) (tick) * 1000)
#define SC_TICK_TO_US(tick) ((sc_tick) tick)
#define SC_TICK_TO_MS(tick) ((sc_tick) (tick) / 1000)
#define SC_TICK_TO_SEC(tick) ((sc_tick) (tick) / 1000000)
#define SC_TICK_FROM_NS(ns) ((sc_tick) (ns) / 1000)
#define SC_TICK_FROM_US(us) ((sc_tick) us)
#define SC_TICK_FROM_MS(ms) ((sc_tick) (ms) * 1000)
#define SC_TICK_FROM_SEC(sec) ((sc_tick) (sec) * 1000000)
sc_tick
sc_tick_now(void);

View File

@@ -62,6 +62,7 @@ void
sc_timeout_stop(struct sc_timeout *timeout) {
sc_mutex_lock(&timeout->mutex);
timeout->stopped = true;
sc_cond_signal(&timeout->cond);
sc_mutex_unlock(&timeout->mutex);
}

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.3.0'
classpath 'com.android.tools.build:gradle:8.7.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
# https://gradle.org/release-checksums/
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -9,13 +9,12 @@
# the server to the device.
.PHONY: default clean \
test \
test test-client test-server \
build-server \
prepare-deps \
prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win64 \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
release
package release
GRADLE ?= ./gradlew
@@ -26,7 +25,7 @@ WIN64_BUILD_DIR := build-win64
VERSION ?= $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
ZIP := zip
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
@@ -34,33 +33,28 @@ WIN64_TARGET := $(WIN64_TARGET_DIR).zip
RELEASE_DIR := release-$(VERSION)
release: clean test build-server zip-win32 zip-win64
mkdir -p "$(RELEASE_DIR)"
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
cd "$(RELEASE_DIR)" && \
sha256sum "scrcpy-server-$(VERSION)" \
"scrcpy-win32-$(VERSION).zip" \
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
@echo "Release generated in $(RELEASE_DIR)/"
release: clean test build-server build-win32 build-win64 package
clean:
$(GRADLE) clean
rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
test:
test-client:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
test-server:
$(GRADLE) -p server check
test: test-client test-server
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
$(GRADLE) -p server assembleRelease
mkdir -p "$(SERVER_BUILD_DIR)/server"
cp server/build/outputs/apk/release/server-release-unsigned.apk \
"$(SERVER_BUILD_DIR)/server/scrcpy-server"
prepare-deps-win32:
@app/deps/adb.sh win32
@@ -86,6 +80,15 @@ build-win32: prepare-deps-win32
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN32_BUILD_DIR)"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$(WIN32_BUILD_DIR)/dist"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/"
cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/"
cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/"
cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/"
cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/"
cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/"
cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/"
build-win64: prepare-deps-win64
rm -rf "$(WIN64_BUILD_DIR)"
@@ -99,33 +102,40 @@ build-win64: prepare-deps-win64
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN64_BUILD_DIR)"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$(WIN64_BUILD_DIR)/dist"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/"
cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/"
cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/"
cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/"
cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/"
cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/"
cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/"
dist-win32: build-server build-win32
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \
zip-win32:
mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)"
cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/"
cd "$(ZIP)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)"
zip-win64: dist-win64
cd "$(DIST)"; \
zip-win64:
mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)"
cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/"
cd "$(ZIP)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)"
package: zip-win32 zip-win64
mkdir -p "$(RELEASE_DIR)"
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
cp "$(ZIP)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
cd "$(RELEASE_DIR)" && \
sha256sum "scrcpy-server-$(VERSION)" \
"scrcpy-win32-$(VERSION).zip" \
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
@echo "Release generated in $(RELEASE_DIR)/"

View File

@@ -2,11 +2,11 @@ apply plugin: 'com.android.application'
android {
namespace 'com.genymobile.scrcpy'
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 34
targetSdkVersion 35
versionCode 20700
versionName "2.7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

View File

@@ -14,8 +14,8 @@ set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.7
PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@@ -45,10 +45,10 @@ EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl
SRC=( \
com/genymobile/scrcpy/*.java \

View File

@@ -0,0 +1,32 @@
package com.genymobile.scrcpy;
import android.os.Build;
/**
* Android version code constants, done right.
* <p/>
* <a href="https://apilevels.com/">API levels</a>
*/
public final class AndroidVersions {
private AndroidVersions() {
// not instantiable
}
public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP;
public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1;
public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M;
public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N;
public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1;
public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O;
public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1;
public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P;
public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q;
public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R;
public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S;
public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2;
public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU;
public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM;
}

View File

@@ -4,7 +4,6 @@ import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Process;
public final class FakeContext extends ContextWrapper {
@@ -32,7 +31,7 @@ public final class FakeContext extends ContextWrapper {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@TargetApi(AndroidVersions.API_31_ANDROID_12)
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);

View File

@@ -475,6 +475,9 @@ public class Options {
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Invalid non-positive size dimension: \"" + size + "\"");
}
return new Size(width, height);
}

View File

@@ -121,7 +121,7 @@ public final class Server {
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) {
Ln.e("Camera mirroring is not supported before Android 12");
throw new ConfigurationException("Camera mirroring is not supported");
}
@@ -190,8 +190,7 @@ public final class Server {
options.getSendFrameMeta());
SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {
surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(),
options.getLockVideoOrientation());
surfaceCapture = new ScreenCapture(device);
} else {
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());

View File

@@ -52,7 +52,7 @@ public final class Workarounds {
}
public static void apply() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
@@ -155,7 +155,7 @@ public final class Workarounds {
}
}
@TargetApi(Build.VERSION_CODES.R)
@TargetApi(AndroidVersions.API_30_ANDROID_11)
@SuppressLint("WrongConstant,MissingPermission")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws
AudioCaptureException {
@@ -226,7 +226,7 @@ public final class Workarounds {
int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE};
int initResult;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12) {
// private native final int native_setup(Object audiorecord_this,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
@@ -252,7 +252,7 @@ public final class Workarounds {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14) {
// private native int native_setup(Object audiorecordThis,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds;
import com.genymobile.scrcpy.util.Ln;
@@ -45,11 +46,11 @@ public class AudioDirectCapture implements AudioCapture {
}
}
@TargetApi(Build.VERSION_CODES.M)
@TargetApi(AndroidVersions.API_23_ANDROID_6_0)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord(int audioSource) {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
@@ -117,7 +118,7 @@ public class AudioDirectCapture implements AudioCapture {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
Ln.w("Audio disabled: it is not supported before Android 11");
throw new AudioCaptureException();
}
@@ -125,7 +126,7 @@ public class AudioDirectCapture implements AudioCapture {
@Override
public void start() throws AudioCaptureException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11) {
startWorkaroundAndroid11();
try {
tryStartRecording(5, 100);
@@ -146,7 +147,7 @@ public class AudioDirectCapture implements AudioCapture {
}
@Override
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Streamer;
@@ -93,7 +94,7 @@ public final class AudioEncoder implements AsyncProcessor {
return format;
}
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@@ -175,9 +176,9 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
@TargetApi(Build.VERSION_CODES.M)
@TargetApi(AndroidVersions.API_23_ANDROID_6_0)
private void encode() throws IOException, ConfigurationException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
@@ -314,7 +315,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
private final class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
@@ -108,7 +109,7 @@ public final class AudioPlaybackCapture implements AudioCapture {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_33_ANDROID_13) {
Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
throw new AudioCaptureException();
}
@@ -130,7 +131,7 @@ public final class AudioPlaybackCapture implements AudioCapture {
}
@Override
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.IO;
@@ -24,7 +25,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
private void record() throws IOException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;

View File

@@ -1,12 +1,12 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.os.Build;
import java.nio.ByteBuffer;
@@ -26,7 +26,7 @@ public class AudioRecordReader {
this.recorder = recorder;
}
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
if (r <= 0) {

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.device.Device;
@@ -243,7 +244,7 @@ public class Controller implements AsyncProcessor {
return false;
}
for (KeyEvent event : events) {
if (!device.injectMainDisplayEvent(event, Device.INJECT_MODE_ASYNC)) {
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
@@ -318,13 +319,13 @@ public class Controller implements AsyncProcessor {
*
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0 && source == InputDevice.SOURCE_MOUSE) {
if (action == MotionEvent.ACTION_DOWN) {
if (actionButton == buttons) {
// First button pressed: ACTION_DOWN
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectVirtualDisplayEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
@@ -335,7 +336,7 @@ public class Controller implements AsyncProcessor {
if (!InputManager.setActionButton(pressEvent, actionButton)) {
return false;
}
if (!device.injectVirtualDisplayEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
@@ -349,7 +350,7 @@ public class Controller implements AsyncProcessor {
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
return false;
}
if (!device.injectVirtualDisplayEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
@@ -357,7 +358,7 @@ public class Controller implements AsyncProcessor {
// Last button released: ACTION_UP
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectVirtualDisplayEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
@@ -368,7 +369,7 @@ public class Controller implements AsyncProcessor {
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
DEFAULT_DEVICE_ID, 0, source, 0);
return device.injectVirtualDisplayEvent(event, Device.INJECT_MODE_ASYNC);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
}
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
@@ -390,7 +391,7 @@ public class Controller implements AsyncProcessor {
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
return device.injectVirtualDisplayEvent(event, Device.INJECT_MODE_ASYNC);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
}
/**
@@ -423,7 +424,7 @@ public class Controller implements AsyncProcessor {
private void getClipboard(int copyKey) {
// On Android >= 7, press the COPY or CUT key if requested
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) {
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
@@ -448,7 +449,7 @@ public class Controller implements AsyncProcessor {
}
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
@@ -38,7 +39,7 @@ public final class UhidManager {
public UhidManager(DeviceMessageSender sender) {
this.sender = sender;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
HandlerThread thread = new HandlerThread("UHidManager");
thread.start();
queue = thread.getLooper().getQueue();
@@ -71,7 +72,7 @@ public final class UhidManager {
}
private void registerUhidListener(int id, FileDescriptor fd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> {
try {
buffer.clear();
@@ -97,7 +98,7 @@ public final class UhidManager {
}
private void unregisterUhidListener(FileDescriptor fd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
queue.removeOnFileDescriptorEventListener(fd);
}
}

View File

@@ -1,7 +1,9 @@
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.ScreenInfo;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl;
@@ -15,6 +17,8 @@ import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
@@ -34,10 +38,26 @@ public final class Device {
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
public interface RotationListener {
void onRotationChanged(int rotation);
}
public interface FoldListener {
void onFoldChanged(int displayId, boolean folded);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
private final Rect crop;
private int maxSize;
private final int lockVideoOrientation;
private Size deviceSize;
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private FoldListener foldListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
@@ -46,15 +66,71 @@ public final class Device {
*/
private final int displayId;
/**
* The surface flinger layer stack associated with this logical display
*/
private final int layerStack;
private final boolean supportsInputEvents;
// set by the ScreenCapture instance
private ScreenInfo screenInfo;
private int virtualDisplayId;
public Device(Options options) {
public Device(Options options) throws ConfigurationException {
displayId = options.getDisplayId();
virtualDisplayId = displayId; // by default
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
throw new ConfigurationException("Unknown display id: " + displayId);
}
int displayInfoFlags = displayInfo.getFlags();
deviceSize = displayInfo.getSize();
crop = options.getCrop();
maxSize = options.getMaxSize();
lockVideoOrientation = options.getLockVideoOrientation();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
layerStack = displayInfo.getLayerStack();
ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
synchronized (Device.this) {
screenInfo = screenInfo.withDeviceRotation(rotation);
// notify
if (rotationListener != null) {
rotationListener.onRotationChanged(rotation);
}
}
}
}, displayId);
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (Device.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
synchronized (Device.this) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
return;
}
deviceSize = displayInfo.getSize();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
// notify
if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded);
}
}
}
});
}
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
@@ -82,20 +158,38 @@ public final class Device {
}
}
// main display or any display on Android >= Q
supportsInputEvents = options.getDisplayId() == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}
// main display or any display on Android >= 10
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
}
public int getDisplayId() {
return displayId;
}
public synchronized void setMaxSize(int newMaxSize) {
maxSize = newMaxSize;
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
}
public synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}
public int getLayerStack() {
return layerStack;
}
public Point getPhysicalPoint(Position position) {
// it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
if (screenInfo == null) {
return null;
}
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
@@ -122,29 +216,13 @@ public final class Device {
}
public static boolean supportsInputEvents(int displayId) {
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
}
public boolean supportsInputEvents() {
return supportsInputEvents;
}
private synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}
public synchronized void setScreenInfo(ScreenInfo screenInfo) {
this.screenInfo = screenInfo;
}
private synchronized int getVirtualDisplayId() {
return virtualDisplayId;
}
public synchronized void setVirtualDisplayId(int virtualDisplayId) {
this.virtualDisplayId = virtualDisplayId;
}
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
if (!supportsInputEvents(displayId)) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
@@ -157,14 +235,10 @@ public final class Device {
return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode);
}
public boolean injectMainDisplayEvent(InputEvent event, int injectMode) {
public boolean injectEvent(InputEvent event, int injectMode) {
return injectEvent(event, displayId, injectMode);
}
public boolean injectVirtualDisplayEvent(InputEvent event, int injectMode) {
return injectEvent(event, virtualDisplayId, injectMode);
}
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
@@ -189,6 +263,14 @@ public final class Device {
return ServiceManager.getPowerManager().isScreenOn();
}
public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener;
}
public synchronized void setFoldListener(FoldListener foldlistener) {
this.foldListener = foldlistener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}
@@ -242,10 +324,10 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
if (applyToMultiPhysicalDisplays
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14
&& Build.BRAND.equalsIgnoreCase("honor")
&& SurfaceControl.hasGetBuildInDisplayMethod()) {
// Workaround for Honor devices with Android 14:
@@ -257,7 +339,7 @@ public final class Device {
if (applyToMultiPhysicalDisplays) {
// On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod();
Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && !SurfaceControl.hasGetPhysicalDisplayIdsMethod();
// Change the power mode for all physical displays
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();

View File

@@ -6,17 +6,15 @@ public final class DisplayInfo {
private final int rotation;
private final int layerStack;
private final int flags;
private final int logicalDensityDpi;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int logicalDensityDpi) {
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
this.displayId = displayId;
this.size = size;
this.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
this.logicalDensityDpi = logicalDensityDpi;
}
public int getDisplayId() {
@@ -38,9 +36,5 @@ public final class DisplayInfo {
public int getFlags() {
return flags;
}
public int getLogicalDensityDpi() {
return logicalDensityDpi;
}
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.BuildConfig;
import android.os.Build;
@@ -31,7 +32,7 @@ public final class IO {
}
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
while (from.hasRemaining()) {
write(fd, from);
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
@@ -34,7 +35,7 @@ public final class Settings {
}
public static String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
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);
@@ -47,7 +48,7 @@ public final class Settings {
}
public static void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
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);
@@ -60,7 +61,7 @@ public final class Settings {
}
public static String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
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);

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.HandlerExecutor;
import com.genymobile.scrcpy.util.Ln;
@@ -20,7 +21,6 @@ import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Range;
@@ -118,7 +118,7 @@ public class CameraCapture extends SurfaceCapture {
return null;
}
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed)
throws CameraAccessException {
if (explicitSize != null) {
@@ -242,7 +242,7 @@ public class CameraCapture extends SurfaceCapture {
}
@SuppressLint("MissingPermission")
@TargetApi(Build.VERSION_CODES.S)
@TargetApi(AndroidVersions.API_31_ANDROID_12)
private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException {
CompletableFuture<CameraDevice> future = new CompletableFuture<>();
ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() {
@@ -289,7 +289,7 @@ public class CameraCapture extends SurfaceCapture {
}
}
@TargetApi(Build.VERSION_CODES.S)
@TargetApi(AndroidVersions.API_31_ANDROID_12)
private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException {
CompletableFuture<CameraCaptureSession> future = new CompletableFuture<>();
OutputConfiguration outputConfig = new OutputConfiguration(surface);
@@ -328,7 +328,7 @@ public class CameraCapture extends SurfaceCapture {
return requestBuilder.build();
}
@TargetApi(Build.VERSION_CODES.S)
@TargetApi(AndroidVersions.API_31_ANDROID_12)
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException {
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
@Override

View File

@@ -1,115 +1,43 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.os.IBinder;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
import android.view.Surface;
public class ScreenCapture extends SurfaceCapture {
public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener {
private final Device device;
private final int displayId;
private int maxSize;
private final Rect crop;
private final int lockVideoOrientation;
private int layerStack;
private int dpi;
private Size deviceSize;
private ScreenInfo screenInfo;
private IBinder display;
private VirtualDisplay virtualDisplay;
private IRotationWatcher rotationWatcher;
private IDisplayFoldListener displayFoldListener;
public ScreenCapture(Device device, int displayId, int maxSize, Rect crop, int lockVideoOrientation) {
public ScreenCapture(Device device) {
this.device = device;
this.displayId = displayId;
this.maxSize = maxSize;
this.crop = crop;
this.lockVideoOrientation = lockVideoOrientation;
}
@Override
public void init() throws ConfigurationException {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
throw new ConfigurationException("Unknown display id: " + displayId);
}
deviceSize = displayInfo.getSize();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
device.setScreenInfo(screenInfo);
layerStack = displayInfo.getLayerStack();
dpi = displayInfo.getLogicalDensityDpi();
if (displayId == 0) {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
synchronized (ScreenCapture.this) {
screenInfo = screenInfo.withDeviceRotation(rotation);
device.setScreenInfo(screenInfo);
}
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
displayFoldListener = new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (ScreenCapture.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
synchronized (ScreenCapture.this) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
return;
}
deviceSize = displayInfo.getSize();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
device.setScreenInfo(screenInfo);
}
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}
if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}
public void init() {
device.setRotationListener(this);
device.setFoldListener(this);
}
@Override
public void start(Surface surface) {
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
if (display != null) {
SurfaceControl.destroyDisplay(display);
@@ -122,12 +50,8 @@ public class ScreenCapture extends SurfaceCapture {
try {
Rect videoRect = screenInfo.getVideoSize().toRect();
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | (1
<< 6) /* DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORT_TOUCH */ | 1 << 8 | 1 << 9 | 1 << 10 | 1 << 11 | 1 << 12 | 1 << 13 | 1 << 14;
virtualDisplay = ServiceManager.getDisplayManager()
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), dpi, surface, flags);
device.setVirtualDisplayId(virtualDisplay.getDisplay().getDisplayId());
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
try {
@@ -144,12 +68,8 @@ public class ScreenCapture extends SurfaceCapture {
@Override
public void release() {
if (rotationWatcher != null) {
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
}
device.setRotationListener(null);
device.setFoldListener(null);
if (display != null) {
SurfaceControl.destroyDisplay(display);
display = null;
@@ -161,23 +81,31 @@ public class ScreenCapture extends SurfaceCapture {
}
@Override
public synchronized Size getSize() {
return screenInfo.getVideoSize();
public Size getSize() {
return device.getScreenInfo().getVideoSize();
}
@Override
public synchronized boolean setMaxSize(int newMaxSize) {
maxSize = newMaxSize;
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
device.setScreenInfo(screenInfo);
public boolean setMaxSize(int maxSize) {
device.setMaxSize(maxSize);
return true;
}
@Override
public void onFoldChanged(int displayId, boolean folded) {
requestReset();
}
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
private static IBinder createDisplay() throws Exception {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(
Build.VERSION.CODENAME));
boolean secure = Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11
&& !"S".equals(Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure);
}

View File

@@ -1,6 +1,5 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Size;
import android.view.Surface;
@@ -35,7 +34,7 @@ public abstract class SurfaceCapture {
/**
* Called once before the capture starts.
*/
public abstract void init() throws ConfigurationException, IOException;
public abstract void init() throws IOException;
/**
* Called after the capture ends (if and only if {@link #init()} has been called).

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Size;
@@ -238,7 +239,7 @@ public class SurfaceEncoder implements AsyncProcessor {
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0) {
format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);
}
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
@@ -7,7 +8,6 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
@@ -63,7 +63,7 @@ public final class ActivityManager {
return removeContentProviderExternalMethod;
}
@TargetApi(Build.VERSION_CODES.Q)
@TargetApi(AndroidVersions.API_29_ANDROID_10)
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
@@ -36,7 +37,7 @@ public final class ClipboardManager {
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
return getPrimaryClipMethod;
}
@@ -99,7 +100,7 @@ public final class ClipboardManager {
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
return setPrimaryClipMethod;
}
@@ -137,7 +138,7 @@ public final class ClipboardManager {
}
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
}
@@ -161,7 +162,7 @@ public final class ClipboardManager {
}
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return;
}
@@ -210,7 +211,7 @@ public final class ClipboardManager {
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return;
}
@@ -230,7 +231,7 @@ public final class ClipboardManager {
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.SettingsException;
@@ -51,7 +52,7 @@ public final class ContentProvider implements Closeable {
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0;
} else {
@@ -79,7 +80,7 @@ public final class ContentProvider implements Closeable {
Method method = getCallMethod();
Object[] args;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12 && callMethodVersion == 0) {
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
} else {
switch (callMethodVersion) {

View File

@@ -1,16 +1,16 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.IBinder;
import java.lang.reflect.Method;
@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"})
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@TargetApi(AndroidVersions.API_34_ANDROID_14)
public final class DisplayControl {
private static final Class<?> CLASS;

View File

@@ -1,18 +1,15 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Command;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.content.Context;
import android.hardware.display.VirtualDisplay;
import android.view.Display;
import android.view.Surface;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
@@ -42,7 +39,7 @@ public final class DisplayManager {
public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) {
Pattern regex = Pattern.compile(
"^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, "
+ "rotation ([0-9]+).*?, density ([0-9]+).*?, layerStack ([0-9]+)",
+ "rotation ([0-9]+).*?, layerStack ([0-9]+)",
Pattern.MULTILINE);
Matcher m = regex.matcher(dumpsysDisplayOutput);
if (!m.find()) {
@@ -52,10 +49,9 @@ public final class DisplayManager {
int width = Integer.parseInt(m.group(2));
int height = Integer.parseInt(m.group(3));
int rotation = Integer.parseInt(m.group(4));
int density = Integer.parseInt(m.group(5));
int layerStack = Integer.parseInt(m.group(6));
int layerStack = Integer.parseInt(m.group(5));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
}
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
@@ -102,8 +98,7 @@ public final class DisplayManager {
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int logicalDensityDpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, logicalDensityDpi);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
@@ -120,17 +115,13 @@ public final class DisplayManager {
private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException {
if (createVirtualDisplayMethod == null) {
createVirtualDisplayMethod = android.hardware.display.DisplayManager.class
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class, int.class);
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class);
}
return createVirtualDisplayMethod;
}
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception {
//Method method = getCreateVirtualDisplayMethod();
Constructor<android.hardware.display.DisplayManager> ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor(Context.class);
ctor.setAccessible(true);
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
//return (VirtualDisplay) method.invoke(null, name, width, height, dpi, surface, flags);
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception {
Method method = getCreateVirtualDisplayMethod();
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
}
}

View File

@@ -2,8 +2,6 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.Method;
@@ -23,9 +21,7 @@ public final class PowerManager {
private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) {
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
isScreenOnMethod = manager.getClass().getMethod(methodName);
isScreenOnMethod = manager.getClass().getMethod("isInteractive");
}
return isScreenOnMethod;
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
@@ -83,9 +84,9 @@ public final class SurfaceControl {
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
if (getBuiltInDisplayMethod == null) {
// the method signature has changed in Android Q
// the method signature has changed in Android 10
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
} else {
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
@@ -106,7 +107,7 @@ public final class SurfaceControl {
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
// call getBuiltInDisplay(0)
return (IBinder) method.invoke(null, 0);
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
@@ -200,29 +201,13 @@ public final class WindowManager {
}
}
public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) {
try {
manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher);
} catch (Exception e) {
Ln.e("Could not unregister rotation watcher", e);
}
}
@TargetApi(29)
@TargetApi(AndroidVersions.API_29_ANDROID_10)
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
Class<?> cls = manager.getClass();
cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
Ln.e("Could not register display fold listener", e);
}
}
@TargetApi(29)
public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
Ln.e("Could not unregister display fold listener", e);
}
}
}