Compare commits
11 Commits
tilt.2
...
turn_scree
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5c2734db9 | ||
|
|
c1f2932db8 | ||
|
|
890ba529c3 | ||
|
|
798727aa58 | ||
|
|
e76ff25e31 | ||
|
|
ec80be1eda | ||
|
|
cdd78273dc | ||
|
|
c546293f35 | ||
|
|
70baf3c384 | ||
|
|
dac0d54c5c | ||
|
|
878bfb1d8b |
@@ -1,4 +1,4 @@
|
|||||||
# scrcpy (v2.3.1)
|
# scrcpy (v2.2)
|
||||||
|
|
||||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||||
|
|
||||||
|
|||||||
@@ -115,12 +115,13 @@ _scrcpy() {
|
|||||||
COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
|
COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--orientation|--display-orientation)
|
--orientation
|
||||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
--display-orientation)
|
||||||
|
COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--record-orientation)
|
--record-orientation)
|
||||||
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
|
COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--lock-video-orientation)
|
--lock-video-orientation)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
|||||||
# For some users, the PATH or ADB environment variables are set from the shell
|
# For some users, the PATH or ADB environment variables are set from the shell
|
||||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||||
# environment correctly initialized.
|
# environment correctly initialized.
|
||||||
Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'"
|
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error"
|
||||||
Icon=scrcpy
|
Icon=scrcpy
|
||||||
Terminal=true
|
Terminal=true
|
||||||
Type=Application
|
Type=Application
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
|||||||
# For some users, the PATH or ADB environment variables are set from the shell
|
# For some users, the PATH or ADB environment variables are set from the shell
|
||||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||||
# environment correctly initialized.
|
# environment correctly initialized.
|
||||||
Exec=/bin/sh -c "\\$SHELL -i -c scrcpy"
|
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy"
|
||||||
Icon=scrcpy
|
Icon=scrcpy
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ endif
|
|||||||
|
|
||||||
cc = meson.get_compiler('c')
|
cc = meson.get_compiler('c')
|
||||||
|
|
||||||
|
crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows'
|
||||||
|
|
||||||
|
if not crossbuild_windows
|
||||||
|
|
||||||
|
# native build
|
||||||
dependencies = [
|
dependencies = [
|
||||||
dependency('libavformat', version: '>= 57.33'),
|
dependency('libavformat', version: '>= 57.33'),
|
||||||
dependency('libavcodec', version: '>= 57.37'),
|
dependency('libavcodec', version: '>= 57.37'),
|
||||||
@@ -114,8 +119,56 @@ if usb_support
|
|||||||
dependencies += dependency('libusb-1.0')
|
dependencies += dependency('libusb-1.0')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
else
|
||||||
|
# cross-compile mingw32 build (from Linux to Windows)
|
||||||
|
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
|
||||||
|
sdl2_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/bin'
|
||||||
|
sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib'
|
||||||
|
sdl2_include_dir = 'prebuilt-deps/data/' + prebuilt_sdl2 + '/include'
|
||||||
|
|
||||||
|
sdl2 = declare_dependency(
|
||||||
|
dependencies: [
|
||||||
|
cc.find_library('SDL2', dirs: sdl2_bin_dir),
|
||||||
|
cc.find_library('SDL2main', dirs: sdl2_lib_dir),
|
||||||
|
],
|
||||||
|
include_directories: include_directories(sdl2_include_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg')
|
||||||
|
ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin'
|
||||||
|
ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include'
|
||||||
|
|
||||||
|
ffmpeg = declare_dependency(
|
||||||
|
dependencies: [
|
||||||
|
cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir),
|
||||||
|
cc.find_library('avformat-60', dirs: ffmpeg_bin_dir),
|
||||||
|
cc.find_library('avutil-58', dirs: ffmpeg_bin_dir),
|
||||||
|
cc.find_library('swresample-4', dirs: ffmpeg_bin_dir),
|
||||||
|
],
|
||||||
|
include_directories: include_directories(ffmpeg_include_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
prebuilt_libusb = meson.get_cross_property('prebuilt_libusb')
|
||||||
|
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin'
|
||||||
|
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include'
|
||||||
|
|
||||||
|
libusb = declare_dependency(
|
||||||
|
dependencies: [
|
||||||
|
cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir),
|
||||||
|
],
|
||||||
|
include_directories: include_directories(libusb_include_dir)
|
||||||
|
)
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
ffmpeg,
|
||||||
|
sdl2,
|
||||||
|
libusb,
|
||||||
|
cc.find_library('mingw32')
|
||||||
|
]
|
||||||
|
|
||||||
|
endif
|
||||||
|
|
||||||
if host_machine.system() == 'windows'
|
if host_machine.system() == 'windows'
|
||||||
dependencies += cc.find_library('mingw32')
|
|
||||||
dependencies += cc.find_library('ws2_32')
|
dependencies += cc.find_library('ws2_32')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
VERSION=6.1-scrcpy-3
|
VERSION=6.1-scrcpy-2
|
||||||
DEP_DIR="ffmpeg-$VERSION"
|
DEP_DIR="ffmpeg-$VERSION"
|
||||||
|
|
||||||
FILENAME="$DEP_DIR".7z
|
FILENAME="$DEP_DIR".7z
|
||||||
SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a
|
SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
VERSION=1.0.26
|
DEP_DIR=libusb-1.0.26
|
||||||
DEP_DIR="libusb-$VERSION"
|
|
||||||
|
|
||||||
FILENAME="libusb-$VERSION-binaries.7z"
|
FILENAME=libusb-1.0.26-binaries.7z
|
||||||
SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5
|
SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
@@ -18,22 +17,17 @@ then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \
|
get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||||
"$FILENAME" "$SHA256SUM"
|
|
||||||
|
|
||||||
mkdir "$DEP_DIR"
|
mkdir "$DEP_DIR"
|
||||||
cd "$DEP_DIR"
|
cd "$DEP_DIR"
|
||||||
|
|
||||||
7z x "../$FILENAME" \
|
7z x "../$FILENAME" \
|
||||||
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
|
libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \
|
||||||
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
|
libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \
|
||||||
"libusb-$VERSION-binaries/libusb-MinGW-x64/" \
|
libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \
|
||||||
"libusb-$VERSION-binaries/libusb-MinGW-x64/"
|
libusb-1.0.26-binaries/libusb-MinGW-x64/include/
|
||||||
|
|
||||||
mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" .
|
mv libusb-1.0.26-binaries/libusb-MinGW-Win32 .
|
||||||
mv "libusb-$VERSION-binaries/libusb-MinGW-x64" .
|
mv libusb-1.0.26-binaries/libusb-MinGW-x64 .
|
||||||
rm -rf "libusb-$VERSION-binaries"
|
rm -rf libusb-1.0.26-binaries
|
||||||
|
|
||||||
# Rename the dll to get the same library name on all platforms
|
|
||||||
mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll
|
|
||||||
mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll
|
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
VERSION=2.28.5
|
DEP_DIR=SDL2-2.28.4
|
||||||
DEP_DIR="SDL2-$VERSION"
|
|
||||||
|
|
||||||
FILENAME="SDL2-devel-$VERSION-mingw.tar.gz"
|
FILENAME=SDL2-devel-2.28.4-mingw.tar.gz
|
||||||
SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21
|
SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
@@ -18,8 +17,7 @@ then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \
|
get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM"
|
||||||
"$FILENAME" "$SHA256SUM"
|
|
||||||
|
|
||||||
mkdir "$DEP_DIR"
|
mkdir "$DEP_DIR"
|
||||||
cd "$DEP_DIR"
|
cd "$DEP_DIR"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ BEGIN
|
|||||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||||
VALUE "OriginalFilename", "scrcpy.exe"
|
VALUE "OriginalFilename", "scrcpy.exe"
|
||||||
VALUE "ProductName", "scrcpy"
|
VALUE "ProductName", "scrcpy"
|
||||||
VALUE "ProductVersion", "2.3.1"
|
VALUE "ProductVersion", "v2.2"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ Use USB device (if there is exactly one, like adb -d).
|
|||||||
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
|
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-disable\-screensaver"
|
.BI "\-\-disable-screensaver"
|
||||||
Disable screensaver while scrcpy is running.
|
Disable screensaver while scrcpy is running.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
@@ -642,11 +642,7 @@ Enable/disable FPS counter (print frames/second in logs)
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+click-and-move
|
.B Ctrl+click-and-move
|
||||||
Pinch-to-zoom and rotate from the center of the screen
|
Pinch-to-zoom from the center of the screen
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Shift+click-and-move
|
|
||||||
Tilt (slide vertically with two fingers)
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Drag & drop APK file
|
.B Drag & drop APK file
|
||||||
|
|||||||
@@ -947,11 +947,7 @@ static const struct sc_shortcut shortcuts[] = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
.shortcuts = { "Ctrl+click-and-move" },
|
.shortcuts = { "Ctrl+click-and-move" },
|
||||||
.text = "Pinch-to-zoom and rotate from the center of the screen",
|
.text = "Pinch-to-zoom from the center of the screen",
|
||||||
},
|
|
||||||
{
|
|
||||||
.shortcuts = { "Shift+click-and-move" },
|
|
||||||
.text = "Tilt (slide vertically with two fingers)",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.shortcuts = { "Drag & drop APK file" },
|
.shortcuts = { "Drag & drop APK file" },
|
||||||
@@ -2142,7 +2138,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->display_orientation = SC_ORIENTATION_180;
|
opts->display_orientation = SC_ORIENTATION_180;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
// rotation 3 was 270° counterclockwise, but orientation
|
// rotation 1 was 270° counterclockwise, but orientation
|
||||||
// is expressed clockwise
|
// is expressed clockwise
|
||||||
opts->display_orientation = SC_ORIENTATION_90;
|
opts->display_orientation = SC_ORIENTATION_90;
|
||||||
break;
|
break;
|
||||||
@@ -2158,7 +2154,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_ORIENTATION: {
|
case OPT_ORIENTATION:
|
||||||
enum sc_orientation orientation;
|
enum sc_orientation orientation;
|
||||||
if (!parse_orientation(optarg, &orientation)) {
|
if (!parse_orientation(optarg, &orientation)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2166,7 +2162,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->display_orientation = orientation;
|
opts->display_orientation = orientation;
|
||||||
opts->record_orientation = orientation;
|
opts->record_orientation = orientation;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case OPT_RENDER_DRIVER:
|
case OPT_RENDER_DRIVER:
|
||||||
opts->render_driver = optarg;
|
opts->render_driver = optarg;
|
||||||
break;
|
break;
|
||||||
@@ -2536,8 +2531,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
if (opts->record_orientation != SC_ORIENTATION_0) {
|
if (opts->record_orientation != SC_ORIENTATION_0) {
|
||||||
if (sc_orientation_is_mirror(opts->record_orientation)) {
|
if (sc_orientation_is_mirror(opts->record_orientation)) {
|
||||||
LOGE("Record orientation only supports rotation, not "
|
LOGE("Record orientation only supports rotation, not "
|
||||||
"flipping: %s",
|
"flipping: %s", optarg);
|
||||||
sc_orientation_get_name(opts->record_orientation));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,9 +227,8 @@ run_demuxer(void *data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Config packets must be merged with the next non-config packet only for
|
// Config packets must be merged with the next non-config packet only for
|
||||||
// H.26x
|
// video streams
|
||||||
bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264
|
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||||
|| raw_codec_id == SC_CODEC_ID_H265;
|
|
||||||
|
|
||||||
struct sc_packet_merger merger;
|
struct sc_packet_merger merger;
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ sc_input_manager_init(struct sc_input_manager *im,
|
|||||||
im->sdl_shortcut_mods.count = shortcut_mods->count;
|
im->sdl_shortcut_mods.count = shortcut_mods->count;
|
||||||
|
|
||||||
im->vfinger_down = false;
|
im->vfinger_down = false;
|
||||||
im->vfinger_invert_x = false;
|
|
||||||
im->vfinger_invert_y = false;
|
|
||||||
|
|
||||||
im->last_keycode = SDLK_UNKNOWN;
|
im->last_keycode = SDLK_UNKNOWN;
|
||||||
im->last_mod = 0;
|
im->last_mod = 0;
|
||||||
@@ -349,14 +347,9 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static struct sc_point
|
static struct sc_point
|
||||||
inverse_point(struct sc_point point, struct sc_size size,
|
inverse_point(struct sc_point point, struct sc_size size) {
|
||||||
bool invert_x, bool invert_y) {
|
|
||||||
if (invert_x) {
|
|
||||||
point.x = size.width - point.x;
|
point.x = size.width - point.x;
|
||||||
}
|
|
||||||
if (invert_y) {
|
|
||||||
point.y = size.height - point.y;
|
point.y = size.height - point.y;
|
||||||
}
|
|
||||||
return point;
|
return point;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,9 +605,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
|||||||
struct sc_point mouse =
|
struct sc_point mouse =
|
||||||
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
||||||
event->y);
|
event->y);
|
||||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
|
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
|
||||||
im->vfinger_invert_x,
|
|
||||||
im->vfinger_invert_y);
|
|
||||||
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
|
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -735,7 +726,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pinch-to-zoom, rotate and tilt simulation.
|
// Pinch-to-zoom simulation.
|
||||||
//
|
//
|
||||||
// If Ctrl is hold when the left-click button is pressed, then
|
// If Ctrl is hold when the left-click button is pressed, then
|
||||||
// pinch-to-zoom mode is enabled: on every mouse event until the left-click
|
// pinch-to-zoom mode is enabled: on every mouse event until the left-click
|
||||||
@@ -744,29 +735,14 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
|||||||
//
|
//
|
||||||
// In other words, the center of the rotation/scaling is the center of the
|
// In other words, the center of the rotation/scaling is the center of the
|
||||||
// screen.
|
// screen.
|
||||||
//
|
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL))
|
||||||
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
|
|
||||||
// can be used instead of Ctrl. The "virtual finger" has a position
|
|
||||||
// inverted with respect to the vertical axis of symmetry in the middle of
|
|
||||||
// the screen.
|
|
||||||
const SDL_Keymod keymod = SDL_GetModState();
|
|
||||||
const bool ctrl_pressed = keymod & KMOD_CTRL;
|
|
||||||
const bool shift_pressed = keymod & KMOD_SHIFT;
|
|
||||||
if (event->button == SDL_BUTTON_LEFT &&
|
if (event->button == SDL_BUTTON_LEFT &&
|
||||||
((down && !im->vfinger_down &&
|
((down && !im->vfinger_down && CTRL_PRESSED) ||
|
||||||
((ctrl_pressed && !shift_pressed) ||
|
|
||||||
(!ctrl_pressed && shift_pressed))) ||
|
|
||||||
(!down && im->vfinger_down))) {
|
(!down && im->vfinger_down))) {
|
||||||
struct sc_point mouse =
|
struct sc_point mouse =
|
||||||
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
|
||||||
event->y);
|
event->y);
|
||||||
if (down) {
|
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
|
||||||
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
|
|
||||||
im->vfinger_invert_y = ctrl_pressed;
|
|
||||||
}
|
|
||||||
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
|
|
||||||
im->vfinger_invert_x,
|
|
||||||
im->vfinger_invert_y);
|
|
||||||
enum android_motionevent_action action = down
|
enum android_motionevent_action action = down
|
||||||
? AMOTION_EVENT_ACTION_DOWN
|
? AMOTION_EVENT_ACTION_DOWN
|
||||||
: AMOTION_EVENT_ACTION_UP;
|
: AMOTION_EVENT_ACTION_UP;
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ struct sc_input_manager {
|
|||||||
} sdl_shortcut_mods;
|
} sdl_shortcut_mods;
|
||||||
|
|
||||||
bool vfinger_down;
|
bool vfinger_down;
|
||||||
bool vfinger_invert_x;
|
|
||||||
bool vfinger_invert_y;
|
|
||||||
|
|
||||||
// Tracks the number of identical consecutive shortcut key down events.
|
// Tracks the number of identical consecutive shortcut key down events.
|
||||||
// Not to be confused with event->repeat, which counts the number of
|
// Not to be confused with event->repeat, which counts the number of
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) {
|
|||||||
// In the final result, we want all the hflips then all the rotations,
|
// In the final result, we want all the hflips then all the rotations,
|
||||||
// so we must move hflip2 to the left:
|
// so we must move hflip2 to the left:
|
||||||
//
|
//
|
||||||
// hflip1 × hflip2 × rotate1' × rotate2
|
// hflip1 × hflip2 × f(rotate1) × rotate2
|
||||||
//
|
//
|
||||||
// with rotate1' = | rotate1 if src is 0° or 180°
|
// with f(rotate1) = | rotate1 if src is 0 or 180
|
||||||
// | rotate1 + 180° if src is 90° or 270°
|
// | rotate1 + 180 if src is 90 or 270
|
||||||
|
|
||||||
src_rotation += 2;
|
src_rotation += 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,21 +419,12 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
sdl_set_hints(options->render_driver);
|
sdl_set_hints(options->render_driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->video_playback ||
|
// Initialize the video subsystem even if --no-video or --no-video-playback
|
||||||
(options->control && options->clipboard_autosync)) {
|
// is passed so that clipboard synchronization still works.
|
||||||
// Initialize the video subsystem even if --no-video or
|
|
||||||
// --no-video-playback is passed so that clipboard synchronization
|
|
||||||
// still works.
|
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/4418>
|
// <https://github.com/Genymobile/scrcpy/issues/4418>
|
||||||
if (SDL_Init(SDL_INIT_VIDEO)) {
|
if (SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
// If it fails, it is an error only if video playback is enabled
|
|
||||||
if (options->video_playback) {
|
|
||||||
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||||
goto end;
|
goto end;
|
||||||
} else {
|
|
||||||
LOGW("Could not initialize SDL video: %s", SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->audio_playback) {
|
if (options->audio_playback) {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
#define SC_SERVER_FILENAME "scrcpy-server"
|
#define SC_SERVER_FILENAME "scrcpy-server"
|
||||||
|
|
||||||
#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
|
#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
|
||||||
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
|
|
||||||
|
|
||||||
#define SC_ADB_PORT_DEFAULT 5555
|
#define SC_ADB_PORT_DEFAULT 5555
|
||||||
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
|
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
|
||||||
@@ -117,7 +116,7 @@ error:
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
push_server(struct sc_intr *intr, const char *serial) {
|
push_server(struct sc_intr *intr, uint32_t scid, const char *serial) {
|
||||||
char *server_path = get_server_path();
|
char *server_path = get_server_path();
|
||||||
if (!server_path) {
|
if (!server_path) {
|
||||||
return false;
|
return false;
|
||||||
@@ -127,7 +126,16 @@ push_server(struct sc_intr *intr, const char *serial) {
|
|||||||
free(server_path);
|
free(server_path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0);
|
|
||||||
|
char *device_server_path;
|
||||||
|
if (asprintf(&device_server_path, "/data/local/tmp/scrcpy-server-%08x.jar",
|
||||||
|
scid) == -1) {
|
||||||
|
LOG_OOM();
|
||||||
|
free(server_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ok = sc_adb_push(intr, serial, server_path, device_server_path, 0);
|
||||||
|
free(device_server_path);
|
||||||
free(server_path);
|
free(server_path);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
@@ -209,13 +217,20 @@ execute_server(struct sc_server *server,
|
|||||||
const char *serial = server->serial;
|
const char *serial = server->serial;
|
||||||
assert(serial);
|
assert(serial);
|
||||||
|
|
||||||
|
char *classpath;
|
||||||
|
if (asprintf(&classpath, "CLASSPATH=/data/local/tmp/scrcpy-server-%08x.jar",
|
||||||
|
params->scid) == -1) {
|
||||||
|
LOG_OOM();
|
||||||
|
return SC_PROCESS_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
const char *cmd[128];
|
const char *cmd[128];
|
||||||
unsigned count = 0;
|
unsigned count = 0;
|
||||||
cmd[count++] = sc_adb_get_executable();
|
cmd[count++] = sc_adb_get_executable();
|
||||||
cmd[count++] = "-s";
|
cmd[count++] = "-s";
|
||||||
cmd[count++] = serial;
|
cmd[count++] = serial;
|
||||||
cmd[count++] = "shell";
|
cmd[count++] = "shell";
|
||||||
cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH;
|
cmd[count++] = classpath;
|
||||||
cmd[count++] = "app_process";
|
cmd[count++] = "app_process";
|
||||||
|
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
@@ -388,6 +403,7 @@ end:
|
|||||||
for (unsigned i = dyn_idx; i < count; ++i) {
|
for (unsigned i = dyn_idx; i < count; ++i) {
|
||||||
free((char *) cmd[i]);
|
free((char *) cmd[i]);
|
||||||
}
|
}
|
||||||
|
free(classpath);
|
||||||
|
|
||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
@@ -937,7 +953,7 @@ run_server(void *data) {
|
|||||||
assert(serial);
|
assert(serial);
|
||||||
LOGD("Device serial: %s", serial);
|
LOGD("Device serial: %s", serial);
|
||||||
|
|
||||||
ok = push_server(&server->intr, serial);
|
ok = push_server(&server->intr, params->scid, serial);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto error_connection_failed;
|
goto error_connection_failed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ system = 'windows'
|
|||||||
cpu_family = 'x86'
|
cpu_family = 'x86'
|
||||||
cpu = 'i686'
|
cpu = 'i686'
|
||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
|
[properties]
|
||||||
|
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win32'
|
||||||
|
prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32'
|
||||||
|
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ system = 'windows'
|
|||||||
cpu_family = 'x86'
|
cpu_family = 'x86'
|
||||||
cpu = 'x86_64'
|
cpu = 'x86_64'
|
||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
|
[properties]
|
||||||
|
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win64'
|
||||||
|
prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32'
|
||||||
|
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
||||||
|
|||||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
|||||||
|
|
||||||
#### Option 2: Use prebuilt server
|
#### Option 2: Use prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v2.3.1`][direct-scrcpy-server]
|
- [`scrcpy-server-v2.2`][direct-scrcpy-server]
|
||||||
<sub>SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b`</sub>
|
<sub>SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874`</sub>
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
|
||||||
|
|
||||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
configuration:
|
configuration:
|
||||||
|
|||||||
@@ -18,17 +18,6 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho
|
|||||||
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
|
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
|
||||||
```
|
```
|
||||||
|
|
||||||
Audio can be disabled:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# audio not captured at all
|
|
||||||
scrcpy --video-source=camera --no-audio
|
|
||||||
scrcpy --video-source=camera --no-audio --record=file.mp4
|
|
||||||
|
|
||||||
# audio captured and recorded, but not played
|
|
||||||
scrcpy --video-source=camera --no-audio-playback --record=file.mp4
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## List
|
## List
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
|||||||
To disable automatic clipboard synchronization, use
|
To disable automatic clipboard synchronization, use
|
||||||
`--no-clipboard-autosync`.
|
`--no-clipboard-autosync`.
|
||||||
|
|
||||||
## Pinch-to-zoom, rotate and tilt simulation
|
## Pinch-to-zoom
|
||||||
|
|
||||||
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
|
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
|
||||||
|
|
||||||
@@ -93,12 +93,8 @@ More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
|
|||||||
Until the left-click button is released, all mouse movements scale and rotate
|
Until the left-click button is released, all mouse movements scale and rotate
|
||||||
the content (if supported by the app) relative to the center of the screen.
|
the content (if supported by the app) relative to the center of the screen.
|
||||||
|
|
||||||
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
|
|
||||||
|
|
||||||
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
|
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
|
||||||
at a location inverted through the center of the screen. When pressing
|
at a location inverted through the center of the screen.
|
||||||
<kbd>Ctrl</kbd> the x and y coordinates are inverted. Using <kbd>Shift</kbd>
|
|
||||||
only inverts x.
|
|
||||||
|
|
||||||
|
|
||||||
## Key repeat
|
## Key repeat
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
|||||||
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||||
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||||
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||||
| Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
|
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
|
||||||
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
|
|
||||||
| Drag & drop APK file | Install APK from computer
|
| Drag & drop APK file | Install APK from computer
|
||||||
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
|
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer
|
|||||||
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
||||||
to create several devices or devices with specific IDs).
|
to create several devices or devices with specific IDs).
|
||||||
|
|
||||||
If you encounter problems detecting your device with Chrome/WebRTC, you can try
|
|
||||||
`exclusive_caps` mode:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo modprobe v4l2loopback exclusive_caps=1
|
|
||||||
```
|
|
||||||
|
|
||||||
To list the enabled devices:
|
To list the enabled devices:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ scrcpy --orientation=270 # 270° clockwise
|
|||||||
scrcpy --orientation=flip0 # hflip
|
scrcpy --orientation=flip0 # hflip
|
||||||
scrcpy --orientation=flip90 # hflip + 90° clockwise
|
scrcpy --orientation=flip90 # hflip + 90° clockwise
|
||||||
scrcpy --orientation=flip180 # vflip (hflip + 180°)
|
scrcpy --orientation=flip180 # vflip (hflip + 180°)
|
||||||
scrcpy --orientation=flip270 # hflip + 270° clockwise
|
scrcpy --orientation=flip270 # hflip + 270°
|
||||||
```
|
```
|
||||||
|
|
||||||
The orientation can be set separately for display and record if necessary, via
|
The orientation can be set separately for display and record if necessary, via
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
Download the [latest release]:
|
Download the [latest release]:
|
||||||
|
|
||||||
- [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit)
|
- [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit)
|
||||||
<sub>SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d`</sub>
|
<sub>SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd`</sub>
|
||||||
- [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit)
|
- [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit)
|
||||||
<sub>SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff`</sub>
|
<sub>SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74`</sub>
|
||||||
|
|
||||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip
|
||||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip
|
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip
|
||||||
|
|
||||||
and extract it.
|
and extract it.
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
BUILDDIR=build-auto
|
BUILDDIR=build-auto
|
||||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
|
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
|
||||||
PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b
|
PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874
|
||||||
|
|
||||||
echo "[scrcpy] Downloading prebuilt server..."
|
echo "[scrcpy] Downloading prebuilt server..."
|
||||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
project('scrcpy', 'c',
|
project('scrcpy', 'c',
|
||||||
version: '2.3.1',
|
version: 'v2.2',
|
||||||
meson_version: '>= 0.48',
|
meson_version: '>= 0.48',
|
||||||
default_options: [
|
default_options: [
|
||||||
'c_std=c11',
|
'c_std=c11',
|
||||||
|
|||||||
60
release.mk
60
release.mk
@@ -69,62 +69,58 @@ prepare-deps:
|
|||||||
@app/prebuilt-deps/prepare-libusb.sh
|
@app/prebuilt-deps/prepare-libusb.sh
|
||||||
|
|
||||||
build-win32: prepare-deps
|
build-win32: prepare-deps
|
||||||
rm -rf "$(WIN32_BUILD_DIR)"
|
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
|
||||||
mkdir -p "$(WIN32_BUILD_DIR)/local"
|
|
||||||
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/"
|
|
||||||
cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/"
|
|
||||||
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/"
|
|
||||||
meson setup "$(WIN32_BUILD_DIR)" \
|
meson setup "$(WIN32_BUILD_DIR)" \
|
||||||
--pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \
|
--cross-file cross_win32.txt \
|
||||||
-Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \
|
--buildtype release --strip -Db_lto=true \
|
||||||
-Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \
|
|
||||||
--cross-file=cross_win32.txt \
|
|
||||||
--buildtype=release --strip -Db_lto=true \
|
|
||||||
-Dcompile_server=false \
|
-Dcompile_server=false \
|
||||||
-Dportable=true
|
-Dportable=true )
|
||||||
ninja -C "$(WIN32_BUILD_DIR)"
|
ninja -C "$(WIN32_BUILD_DIR)"
|
||||||
|
|
||||||
build-win64: prepare-deps
|
build-win64: prepare-deps
|
||||||
rm -rf "$(WIN64_BUILD_DIR)"
|
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
|
||||||
mkdir -p "$(WIN64_BUILD_DIR)/local"
|
|
||||||
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/"
|
|
||||||
cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/"
|
|
||||||
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/"
|
|
||||||
meson setup "$(WIN64_BUILD_DIR)" \
|
meson setup "$(WIN64_BUILD_DIR)" \
|
||||||
--pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \
|
--cross-file cross_win64.txt \
|
||||||
-Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \
|
--buildtype release --strip -Db_lto=true \
|
||||||
-Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \
|
|
||||||
--cross-file=cross_win64.txt \
|
|
||||||
--buildtype=release --strip -Db_lto=true \
|
|
||||||
-Dcompile_server=false \
|
-Dcompile_server=false \
|
||||||
-Dportable=true
|
-Dportable=true )
|
||||||
ninja -C "$(WIN64_BUILD_DIR)"
|
ninja -C "$(WIN64_BUILD_DIR)"
|
||||||
|
|
||||||
dist-win32: build-server build-win32
|
dist-win32: build-server build-win32
|
||||||
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
|
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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 "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/data/scrcpy-console.bat "$(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/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/data/icon.png "$(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/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
|
||||||
dist-win64: build-server build-win64
|
dist-win64: build-server build-win64
|
||||||
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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 "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/data/scrcpy-console.bat "$(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/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/data/icon.png "$(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/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
|
||||||
zip-win32: dist-win32
|
zip-win32: dist-win32
|
||||||
cd "$(DIST)"; \
|
cd "$(DIST)"; \
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode 20301
|
versionCode 200
|
||||||
versionName "2.3.1"
|
versionName "v2.2"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRCPY_DEBUG=false
|
SCRCPY_DEBUG=false
|
||||||
SCRCPY_VERSION_NAME=2.3.1
|
SCRCPY_VERSION_NAME=v2.2
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-34}
|
PLATFORM=${ANDROID_PLATFORM:-34}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ public interface AsyncProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void start(TerminationListener listener);
|
void start(TerminationListener listener);
|
||||||
|
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
void join() throws InterruptedException;
|
void join() throws InterruptedException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,7 +289,8 @@ public class CameraCapture extends SurfaceCapture {
|
|||||||
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
|
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
|
||||||
|
|
||||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor,
|
||||||
|
new CameraCaptureSession.StateCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onConfigured(CameraCaptureSession session) {
|
public void onConfigured(CameraCaptureSession session) {
|
||||||
future.complete(session);
|
future.complete(session);
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ public final class CleanUp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String... args) {
|
public static void main(String... args) {
|
||||||
unlinkSelf();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for the server to die
|
// Wait for the server to die
|
||||||
System.in.read();
|
System.in.read();
|
||||||
@@ -185,9 +183,11 @@ public final class CleanUp {
|
|||||||
} else if (config.restoreNormalPowerMode) {
|
} else if (config.restoreNormalPowerMode) {
|
||||||
Ln.i("Restoring normal power mode");
|
Ln.i("Restoring normal power mode");
|
||||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||||
|
// Make sure the request is performed before exiting
|
||||||
|
DisplayPowerMode.stopAndJoin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.exit(0);
|
unlinkSelf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,8 +318,9 @@ public class Controller implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
MotionEvent event = MotionEvent
|
||||||
DEFAULT_DEVICE_ID, 0, source, 0);
|
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||||
|
0);
|
||||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,8 +341,9 @@ public class Controller implements AsyncProcessor {
|
|||||||
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
|
||||||
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
|
||||||
|
|
||||||
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
|
MotionEvent event = MotionEvent
|
||||||
DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
|
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0,
|
||||||
|
InputDevice.SOURCE_MOUSE, 0);
|
||||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||||
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
|
||||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
@@ -12,8 +11,8 @@ import android.graphics.Rect;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.IDisplayFoldListener;
|
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
|
import android.view.IDisplayFoldListener;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
import android.view.KeyCharacterMap;
|
import android.view.KeyCharacterMap;
|
||||||
@@ -45,11 +44,11 @@ public final class Device {
|
|||||||
void onClipboardTextChanged(String text);
|
void onClipboardTextChanged(String text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Size deviceSize;
|
||||||
private final Rect crop;
|
private final Rect crop;
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private final int lockVideoOrientation;
|
private final int lockVideoOrientation;
|
||||||
|
|
||||||
private Size deviceSize;
|
|
||||||
private ScreenInfo screenInfo;
|
private ScreenInfo screenInfo;
|
||||||
private RotationListener rotationListener;
|
private RotationListener rotationListener;
|
||||||
private FoldListener foldListener;
|
private FoldListener foldListener;
|
||||||
@@ -116,8 +115,8 @@ public final class Device {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceSize = displayInfo.getSize();
|
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
|
||||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
|
options.getMaxSize(), options.getLockVideoOrientation());
|
||||||
// notify
|
// notify
|
||||||
if (foldListener != null) {
|
if (foldListener != null) {
|
||||||
foldListener.onFoldChanged(displayId, folded);
|
foldListener.onFoldChanged(displayId, folded);
|
||||||
@@ -316,12 +315,16 @@ public final class Device {
|
|||||||
*/
|
*/
|
||||||
public static boolean setScreenPowerMode(int mode) {
|
public static boolean setScreenPowerMode(int mode) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
// On Android 14, these internal methods have been moved to DisplayControl
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod()) {
|
||||||
boolean useDisplayControl =
|
// On Android 14+, these internal methods have been moved to system server classes.
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod();
|
// Run a separate process with the correct classpath and LD_PRELOAD to change the display power mode.
|
||||||
|
DisplayPowerMode.setRemoteDisplayPowerMode(mode);
|
||||||
|
// The call is asynchronous (we don't want to block)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Change the power mode for all physical displays
|
// Change the power mode for all physical displays
|
||||||
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();
|
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||||
if (physicalDisplayIds == null) {
|
if (physicalDisplayIds == null) {
|
||||||
Ln.e("Could not get physical display ids");
|
Ln.e("Could not get physical display ids");
|
||||||
return false;
|
return false;
|
||||||
@@ -329,8 +332,7 @@ public final class Device {
|
|||||||
|
|
||||||
boolean allOk = true;
|
boolean allOk = true;
|
||||||
for (long physicalDisplayId : physicalDisplayIds) {
|
for (long physicalDisplayId : physicalDisplayIds) {
|
||||||
IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken(
|
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||||
physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
|
||||||
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
|
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||||
}
|
}
|
||||||
return allOk;
|
return allOk;
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ public final class DeviceMessageSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
thread = new Thread(() -> {
|
thread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
184
server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java
Normal file
184
server/src/main/java/com/genymobile/scrcpy/DisplayPowerMode.java
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Android 14, the methods used to turn the device screen off have been moved from SurfaceControl (in framework.jar) to DisplayControl (a system
|
||||||
|
* server class). As a consequence, they could not be called directly. See {@url https://github.com/Genymobile/scrcpy/issues/3927}.
|
||||||
|
* <p>
|
||||||
|
* Instead, run a separate process with a different classpath and LD_PRELOAD just to set the display power mode. The scrcpy server can request to
|
||||||
|
* this process to set the display mode by writing the mode (a single byte, the value of one of the SurfaceControl.POWER_MODE_* constants,
|
||||||
|
* typically 0=off, 2=on) to the process stdin. In return, it receives the status of the request (0=ok, 1=error) on the process stdout.
|
||||||
|
* <p>
|
||||||
|
* This separate process is started on the first display mode request.
|
||||||
|
* <p>
|
||||||
|
* Since the client must not block, and calling/joining a process is blocking (this specific one takes a few hundred milliseconds to complete),
|
||||||
|
* this class uses an internal thread to execute the requests asynchronously, and serialize them (so that two successive requests are guaranteed to
|
||||||
|
* be executed in order). In addition, it only executes the last pending request (setting power mode to value X then to value Y is equivalent to
|
||||||
|
* just setting it to value Y).
|
||||||
|
*/
|
||||||
|
public final class DisplayPowerMode {
|
||||||
|
|
||||||
|
private static final Proxy PROXY = new Proxy();
|
||||||
|
|
||||||
|
private static final class Proxy implements Runnable {
|
||||||
|
|
||||||
|
private Process process;
|
||||||
|
private Thread thread;
|
||||||
|
private int requestedMode = -1;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
synchronized boolean requestMode(int mode) {
|
||||||
|
try {
|
||||||
|
if (process == null) {
|
||||||
|
process = executeDisplayPowerModeDaemon();
|
||||||
|
thread = new Thread(this, "DisplayPowerModeProxy");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
requestedMode = mode;
|
||||||
|
notify();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Ln.e("Could not start display power mode daemon", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopAndJoin() {
|
||||||
|
boolean hasThread;
|
||||||
|
synchronized (this) {
|
||||||
|
hasThread = thread != null;
|
||||||
|
if (thread != null) {
|
||||||
|
stopped = true;
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasThread) {
|
||||||
|
// Join the thread without holding the mutex (that would cause a deadlock)
|
||||||
|
try {
|
||||||
|
thread.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Ln.e("Thread join interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
OutputStream out = process.getOutputStream();
|
||||||
|
InputStream in = process.getInputStream();
|
||||||
|
while (true) {
|
||||||
|
int mode;
|
||||||
|
synchronized (this) {
|
||||||
|
while (!stopped && requestedMode == -1) {
|
||||||
|
wait();
|
||||||
|
}
|
||||||
|
mode = requestedMode;
|
||||||
|
requestedMode = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if stopped, the last request must be executed to restore the display power mode to normal
|
||||||
|
if (mode == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
out.write(mode);
|
||||||
|
out.flush();
|
||||||
|
int status = in.read();
|
||||||
|
if (status != 0) {
|
||||||
|
Ln.e("Set display power mode failed remotely: status=" + status);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Ln.e("Could not request display power mode", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// end of thread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DisplayPowerMode() {
|
||||||
|
// not instantiable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the scrcpy process
|
||||||
|
public static boolean setRemoteDisplayPowerMode(int mode) {
|
||||||
|
return PROXY.requestMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stopAndJoin() {
|
||||||
|
PROXY.stopAndJoin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the proxy thread in the scrcpy process
|
||||||
|
private static Process executeDisplayPowerModeDaemon() throws IOException {
|
||||||
|
String[] ldPreloadLibs = {"/system/lib64/libandroid_servers.so"};
|
||||||
|
String[] cmd = {"app_process", "/", DisplayPowerMode.class.getName()};
|
||||||
|
|
||||||
|
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||||
|
builder.environment().put("LD_PRELOAD", String.join(" ", ldPreloadLibs));
|
||||||
|
builder.environment().put("CLASSPATH", Server.SERVER_PATH + ":/system/framework/services.jar");
|
||||||
|
return builder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executed in the DisplayPowerMode-specific process
|
||||||
|
@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi"})
|
||||||
|
private static void setDisplayPowerModeUsingDisplayControl(int mode) throws Exception {
|
||||||
|
System.loadLibrary("android_servers");
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
Class<?> displayControlClass = Class.forName("com.android.server.display.DisplayControl");
|
||||||
|
Method getPhysicalDisplayIdsMethod = displayControlClass.getDeclaredMethod("getPhysicalDisplayIds");
|
||||||
|
Method getPhysicalDisplayTokenMethod = displayControlClass.getDeclaredMethod("getPhysicalDisplayToken", long.class);
|
||||||
|
|
||||||
|
Class<?> surfaceControlClass = Class.forName("android.view.SurfaceControl");
|
||||||
|
Method setDisplayPowerModeMethod = surfaceControlClass.getDeclaredMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||||
|
|
||||||
|
long[] displayIds = (long[]) getPhysicalDisplayIdsMethod.invoke(null);
|
||||||
|
for (long displayId : displayIds) {
|
||||||
|
Object token = getPhysicalDisplayTokenMethod.invoke(null, displayId);
|
||||||
|
setDisplayPowerModeMethod.invoke(null, token, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String... args) {
|
||||||
|
// This process uses stdin/stdout to communicate with the caller, make sure nothing else writes to stdout
|
||||||
|
// (and never use Ln methods other than Ln.w() and Ln.e()).
|
||||||
|
PrintStream nullStream = new PrintStream(new Ln.NullOutputStream());
|
||||||
|
System.setOut(nullStream);
|
||||||
|
PrintStream stdout = Ln.CONSOLE_OUT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
// Wait for requests
|
||||||
|
int request = System.in.read();
|
||||||
|
if (request == -1) {
|
||||||
|
// EOF
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setDisplayPowerModeUsingDisplayControl(request);
|
||||||
|
stdout.write(0); // ok
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Ln.e("Could not set display power mode", e);
|
||||||
|
stdout.write(1); // error
|
||||||
|
}
|
||||||
|
stdout.flush();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Expected when the server is dead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,8 @@ public final class Ln {
|
|||||||
private static final String TAG = "scrcpy";
|
private static final String TAG = "scrcpy";
|
||||||
private static final String PREFIX = "[server] ";
|
private static final String PREFIX = "[server] ";
|
||||||
|
|
||||||
private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
|
public static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
|
||||||
private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
|
public static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
|
||||||
|
|
||||||
enum Level {
|
enum Level {
|
||||||
VERBOSE, DEBUG, INFO, WARN, ERROR
|
VERBOSE, DEBUG, INFO, WARN, ERROR
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import java.util.List;
|
|||||||
public final class Server {
|
public final class Server {
|
||||||
|
|
||||||
public static final String SERVER_PATH;
|
public static final String SERVER_PATH;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
|
String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
|
||||||
// By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
|
// By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ public final class Workarounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void apply(boolean audio, boolean camera) {
|
public static void apply(boolean audio, boolean camera) {
|
||||||
boolean mustFillConfigurationController = false;
|
|
||||||
boolean mustFillAppInfo = false;
|
boolean mustFillAppInfo = false;
|
||||||
boolean mustFillAppContext = false;
|
boolean mustFillAppContext = false;
|
||||||
|
|
||||||
@@ -86,23 +85,11 @@ public final class Workarounds {
|
|||||||
mustFillAppContext = true;
|
mustFillAppContext = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
// 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.
|
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/4467>
|
|
||||||
mustFillConfigurationController = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mustFillConfigurationController) {
|
|
||||||
// Must be call before fillAppContext() because it is necessary to get a valid system context
|
|
||||||
fillConfigurationController();
|
|
||||||
}
|
|
||||||
if (mustFillAppInfo) {
|
if (mustFillAppInfo) {
|
||||||
fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
if (mustFillAppContext) {
|
if (mustFillAppContext) {
|
||||||
fillAppContext();
|
Workarounds.fillAppContext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,22 +149,6 @@ public final class Workarounds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fillConfigurationController() {
|
|
||||||
try {
|
|
||||||
Class<?> configurationControllerClass = Class.forName("android.app.ConfigurationController");
|
|
||||||
Class<?> activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal");
|
|
||||||
Constructor<?> configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass);
|
|
||||||
configurationControllerConstructor.setAccessible(true);
|
|
||||||
Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD);
|
|
||||||
|
|
||||||
Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController");
|
|
||||||
configurationControllerField.setAccessible(true);
|
|
||||||
configurationControllerField.set(ACTIVITY_THREAD, configurationController);
|
|
||||||
} catch (Throwable throwable) {
|
|
||||||
Ln.d("Could not fill configuration: " + throwable.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Context getSystemContext() {
|
static Context getSystemContext() {
|
||||||
try {
|
try {
|
||||||
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
|
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
|
||||||
@@ -285,28 +256,16 @@ public final class Workarounds {
|
|||||||
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
|
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
|
||||||
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
|
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
// private native int native_setup(Object audiorecordThis,
|
// private native int native_setup(Object audiorecordThis,
|
||||||
// Object /*AudioAttributes*/ attributes,
|
// Object /*AudioAttributes*/ attributes,
|
||||||
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
|
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
|
||||||
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
|
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
|
||||||
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
|
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
|
||||||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
|
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
|
||||||
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
|
int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
|
||||||
nativeSetupMethod.setAccessible(true);
|
nativeSetupMethod.setAccessible(true);
|
||||||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
|
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
|
||||||
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
|
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0);
|
||||||
attributionSourceParcel, 0L, 0);
|
|
||||||
} else {
|
|
||||||
// Android 14 added a new int parameter "halInputFlags"
|
|
||||||
// <https://github.com/aosp-mirror/platform_frameworks_base/commit/f6135d75db79b1d48fad3a3b3080d37be20a2313>
|
|
||||||
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
|
|
||||||
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class);
|
|
||||||
nativeSetupMethod.setAccessible(true);
|
|
||||||
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
|
|
||||||
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
|
|
||||||
attributionSourceParcel, 0L, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,8 @@ public final class ClipboardManager {
|
|||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
||||||
getMethodVersion = 2;
|
getMethodVersion = 2;
|
||||||
} catch (NoSuchMethodException e3) {
|
} catch (NoSuchMethodException e3) {
|
||||||
try {
|
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
|
||||||
getMethodVersion = 3;
|
getMethodVersion = 3;
|
||||||
} catch (NoSuchMethodException e4) {
|
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
|
|
||||||
getMethodVersion = 4;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,11 +87,8 @@ public final class ClipboardManager {
|
|||||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||||
case 2:
|
case 2:
|
||||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
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);
|
|
||||||
default:
|
default:
|
||||||
// The last boolean parameter is "userOperate"
|
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
|
||||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +138,8 @@ public final class ClipboardManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
|
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
|
||||||
|
|
||||||
import com.genymobile.scrcpy.Ln;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"})
|
|
||||||
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
||||||
public final class DisplayControl {
|
|
||||||
|
|
||||||
private static final Class<?> CLASS;
|
|
||||||
|
|
||||||
static {
|
|
||||||
Class<?> displayControlClass = null;
|
|
||||||
try {
|
|
||||||
Class<?> classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory");
|
|
||||||
Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class,
|
|
||||||
ClassLoader.class, int.class, boolean.class, String.class);
|
|
||||||
ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null,
|
|
||||||
ClassLoader.getSystemClassLoader(), 0, true, null);
|
|
||||||
|
|
||||||
displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl");
|
|
||||||
|
|
||||||
Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class);
|
|
||||||
loadMethod.setAccessible(true);
|
|
||||||
loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers");
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Ln.e("Could not initialize DisplayControl", e);
|
|
||||||
// Do not throw an exception here, the methods will fail when they are called
|
|
||||||
}
|
|
||||||
CLASS = displayControlClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method getPhysicalDisplayTokenMethod;
|
|
||||||
private static Method getPhysicalDisplayIdsMethod;
|
|
||||||
|
|
||||||
private DisplayControl() {
|
|
||||||
// only static methods
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
|
|
||||||
if (getPhysicalDisplayTokenMethod == null) {
|
|
||||||
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
|
|
||||||
}
|
|
||||||
return getPhysicalDisplayTokenMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
|
|
||||||
try {
|
|
||||||
Method method = getGetPhysicalDisplayTokenMethod();
|
|
||||||
return (IBinder) method.invoke(null, physicalDisplayId);
|
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not invoke method", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
|
|
||||||
if (getPhysicalDisplayIdsMethod == null) {
|
|
||||||
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
|
|
||||||
}
|
|
||||||
return getPhysicalDisplayIdsMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long[] getPhysicalDisplayIds() {
|
|
||||||
try {
|
|
||||||
Method method = getGetPhysicalDisplayIdsMethod();
|
|
||||||
return (long[]) method.invoke(null);
|
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not invoke method", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ import java.lang.reflect.Method;
|
|||||||
public final class ServiceManager {
|
public final class ServiceManager {
|
||||||
|
|
||||||
private static final Method GET_SERVICE_METHOD;
|
private static final Method GET_SERVICE_METHOD;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
|
GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import com.genymobile.scrcpy.Ln;
|
|||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
import android.view.IDisplayFoldListener;
|
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
|
import android.view.IDisplayFoldListener;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|||||||
Reference in New Issue
Block a user