Compare commits

..

18 Commits

Author SHA1 Message Date
870ced088e Name server parameters
The options values to configure the server were identified by their
command-line argument index.

Now that there are a lot of arguments, many of them being booleans, it
became unreadable and error-prone.

Identify the arguments by a key string instead.
2020-05-06 00:40:33 +02:00
a845ea0794 Allocate and format server command args
This paves the way to name all parameters.
2020-05-06 00:39:37 +02:00
a0d98fe4ae Add asprintf() compat function
Add sc_asprintf() similar to asprintf() which is not available
everywhere.
2020-05-06 00:18:52 +02:00
74ece9b45b Simplify ScreenEncoder more
Commit 927d655ff6 removed the
iFrameInternal field and constructor argument.

Also remove it from createFormat() arguments.
2020-05-05 19:01:44 +02:00
c77024314d Add an option to keep the device awake
Add an option to prevent the device to sleep:

    scrcpy --stay-awake
    scrcpy -w

The initial state is restored on exit.

Fixes #631 <https://github.com/Genymobile/scrcpy/issues/631>
2020-05-02 02:14:25 +02:00
828327365a Reorder options in alphabetical order 2020-05-02 01:55:32 +02:00
4668638ee1 Handle "show touches" on the device-side
Now that the server can access the Android settings and clean up
properly, handle the "show touches" option from the server.

The initial state is now correctly restored, even on device
disconnection.
2020-05-02 01:55:30 +02:00
dbb0df607c Move constants to ServiceManager
PACKAGE_NAME and USER_ID could be use by several "managers", so move
them to the service manager.
2020-05-02 01:22:18 +02:00
2f74ec2518 Add a clean up process on the device
In order to clean up on close, use a separate process which is not
killed when the device is disconnected (even if the main process itself
is killed).
2020-05-02 01:22:18 +02:00
8c6799297b Implement access to settings without Context
Expose methods to access the Android settings using private APIs.

This allows to read and write settings values immediately without
starting a new process to call "settings".
2020-05-02 01:22:10 +02:00
62c0c1321f Apply workarounds only on error
To avoid NullPointerException on some devices, workarounds have been
implemented. But these workaround produce (harmless) internal errors
causing exceptions to be printed in the console.

To avoid this problem, apply the workarounds only if it fails without
them.

Fixes #994 <https://github.com/Genymobile/scrcpy/issues/994>
Refs #365 <https://github.com/Genymobile/scrcpy/issues/365>
Refs #940 <https://github.com/Genymobile/scrcpy/issues/940>
2020-05-01 19:42:31 +02:00
d4eeb1c84d Fix AutoAdb url
PR #1344 <https://github.com/Genymobile/scrcpy/pull/1344>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-05-01 19:34:08 +02:00
4e9e712312 Update links to v1.13 in README and BUILD 2020-04-29 22:56:26 +02:00
9babe26805 Bump version to 1.13 2020-04-29 22:24:08 +02:00
76567e684a Upgrade SDL (2.0.12) for Windows
Include the latest version of SDL in Windows releases.
2020-04-27 21:45:15 +02:00
b55ca127f8 Upgrade FFmpeg (4.2.2) for Windows
Include the latest version of FFmpeg in Windows releases.
2020-04-27 21:45:15 +02:00
a12b938234 Merge branch 'master' into dev 2020-04-27 21:44:59 +02:00
561ede444e Mention Ubuntu 20.04 package
Ubuntu 20.04 has been released today, and scrcpy is available in their
repositories: <https://packages.ubuntu.com/focal/scrcpy>
2020-04-23 16:36:48 +02:00
30 changed files with 650 additions and 276 deletions

View File

@ -249,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.12.1`][direct-scrcpy-server]
_(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_
- [`scrcpy-server-v1.13`][direct-scrcpy-server]
_(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View File

@ -100,30 +100,30 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
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 "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \

View File

@ -1,4 +1,4 @@
# scrcpy (v1.12.1)
# scrcpy (v1.13)
This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
@ -37,7 +37,7 @@ control it using keyboard and mouse.
### Linux
In Debian (_testing_ and _sid_ for now):
On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04):
```
apt install scrcpy
@ -69,10 +69,10 @@ hard).
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
- [`scrcpy-win64-v1.12.1.zip`][direct-win64]
_(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_
- [`scrcpy-win64-v1.13.zip`][direct-win64]
_(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip
It is also available in [Chocolatey]:
@ -270,7 +270,7 @@ You could use [AutoAdb]:
autoadb scrcpy -s '{}'
```
[AutoAdb]: https://github.com/rom1v/usbaudio
[AutoAdb]: https://github.com/rom1v/autoadb
#### SSH tunnel
@ -396,6 +396,18 @@ The list of display ids can be retrieved by:
adb shell dumpsys display # search "mDisplayId=" in the output
```
#### Stay awake
To prevent the device to sleep after some delay:
```bash
scrcpy --stay-awake
scrcpy -w
```
The initial state is restored when scrcpy is closed.
#### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
@ -410,6 +422,14 @@ Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
It can be useful to also prevent the device to sleep:
```bash
scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
#### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
@ -429,7 +449,8 @@ device).
Android provides this feature in _Developers options_.
_Scrcpy_ provides an option to enable this feature on start and disable on exit:
_Scrcpy_ provides an option to enable this feature on start and restore the
initial value on exit:
```bash
scrcpy --show-touches

View File

@ -116,6 +116,9 @@ conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))

View File

@ -136,7 +136,7 @@ Turn the device screen off immediately.
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, disable on quit.
Enable "show touches" on start, restore the initial value on exit..
It only shows physical touches (not clicks from scrcpy).
@ -144,6 +144,10 @@ It only shows physical touches (not clicks from scrcpy).
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running.
.TP
.B \-\-window\-borderless
Disable window decorations (display borderless window).

View File

@ -130,12 +130,16 @@ scrcpy_print_usage(const char *arg0) {
" Turn the device screen off immediately.\n"
"\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n"
" Enable \"show touches\" on start, restore the initial value\n"
" on exit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
" -w, --stay-awake\n"
" Keep the device on while scrcpy is running.\n"
"\n"
" --window-borderless\n"
" Disable window decorations (display borderless window).\n"
"\n"
@ -486,6 +490,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"no-display", no_argument, NULL, 'N'},
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
{"port", required_argument, NULL, 'p'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
@ -495,8 +500,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"rotation", required_argument, NULL, OPT_ROTATION},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-x", required_argument, NULL, OPT_WINDOW_X},
@ -513,7 +518,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
optind = 0; // reset to start from the first argument in tests
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options,
NULL)) != -1) {
switch (c) {
case 'b':
@ -593,6 +598,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case 'v':
args->version = true;
break;
case 'w':
opts->stay_awake = true;
break;
case OPT_RENDER_EXPIRED_FRAMES:
opts->render_expired_frames = true;
break;
@ -675,5 +683,10 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
return false;
}
if (!opts->control && opts->stay_awake) {
LOGE("Could not request to stay awake if control is disabled");
return false;
}
return true;
}

View File

@ -128,7 +128,6 @@ enum event_result {
EVENT_RESULT_CONTINUE,
EVENT_RESULT_STOPPED_BY_USER,
EVENT_RESULT_STOPPED_BY_EOS,
EVENT_RESULT_STOPPED_BY_ERROR,
};
static enum event_result
@ -151,9 +150,7 @@ handle_event(SDL_Event *event, bool control) {
}
break;
case SDL_WINDOWEVENT:
if (!screen_handle_window_event(&screen, &event->window)) {
return EVENT_RESULT_STOPPED_BY_ERROR;
}
screen_handle_window_event(&screen, &event->window);
break;
case SDL_TEXTINPUT:
if (!control) {
@ -225,9 +222,6 @@ event_loop(bool display, bool control) {
case EVENT_RESULT_STOPPED_BY_EOS:
LOGW("Device disconnected");
return false;
case EVENT_RESULT_STOPPED_BY_ERROR:
LOGC("Stopping due to unrecoverable error");
return false;
case EVENT_RESULT_CONTINUE:
break;
}
@ -235,21 +229,6 @@ event_loop(bool display, bool control) {
return false;
}
static process_t
set_show_touches_enabled(const char *serial, bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void
wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
@ -298,19 +277,13 @@ scrcpy(const struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
};
if (!server_start(&server, options->serial, &params)) {
return false;
}
process_t proc_show_touches = PROCESS_NONE;
bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, true);
show_touches_waited = false;
}
bool ret = false;
bool fps_counter_initialized = false;
@ -427,11 +400,6 @@ scrcpy(const struct scrcpy_options *options) {
}
}
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = true;
}
input_manager.prefer_text = options->prefer_text;
ret = event_loop(options->display, options->control);
@ -488,16 +456,6 @@ end:
fps_counter_destroy(&fps_counter);
}
if (options->show_touches) {
if (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, false);
wait_show_touches(proc_show_touches);
}
server_destroy(&server);
return ret;

View File

@ -38,6 +38,7 @@ struct scrcpy_options {
bool prefer_text;
bool window_borderless;
bool mipmaps;
bool stay_awake;
};
#define SCRCPY_OPTIONS_DEFAULT { \
@ -72,6 +73,7 @@ struct scrcpy_options {
.prefer_text = false, \
.window_borderless = false, \
.mipmaps = true, \
.stay_awake = false, \
}
bool

View File

@ -217,9 +217,10 @@ screen_init_rendering(struct screen *screen, const char *window_title,
struct size window_size =
get_initial_optimal_size(content_size, window_width, window_height);
uint32_t window_flags = SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_ALLOW_HIGHDPI;
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
if (always_on_top) {
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
@ -309,11 +310,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
screen->windowed_window_size = window_size;
struct scale_ratio *rw = &screen->scale.w;
struct scale_ratio *rh = &screen->scale.h;
SDL_GetWindowSize(screen->window, &rw->window, &rh->window);
SDL_GL_GetDrawableSize(screen->window, &rw->drawable, &rh->drawable);
return true;
}
@ -523,74 +519,11 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
content_size.height);
}
bool
screen_fix_hidpi(struct screen *screen) {
// If the scale ratio has changed, recreate the renderer to fix HiDPI issues
// <https://github.com/Genymobile/scrcpy/issues/15>
int ww, wh, dw, dh;
SDL_GetWindowSize(screen->window, &ww, &wh);
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
struct scale_ratio *rw = &screen->scale.w;
struct scale_ratio *rh = &screen->scale.h;
if (ww * rw->drawable == dw * rw->window &&
wh * rh->drawable == dh * rh->window) {
// same ratio, both horizontally and vertically, nothing to do
return true;
}
// update the current scale
rw->window = ww;
rh->window = wh;
rw->drawable = dw;
rh->drawable = dh;
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
if (screen->renderer) {
SDL_DestroyRenderer(screen->renderer);
}
screen->renderer = SDL_CreateRenderer(screen->window, -1,
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError());
return false;
}
struct size content_size = screen->content_size;
if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width,
content_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
SDL_DestroyRenderer(screen->renderer);
return false;
}
// FIXME this is wrong, we must update the last frame to the new texture,
// but we don't have it anymore!
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
SDL_DestroyRenderer(screen->renderer);
return false;
}
LOGD("Renderer and texture reset");
return true;
}
bool
void
screen_handle_window_event(struct screen *screen,
const SDL_WindowEvent *event) {
switch (event->event) {
case SDL_WINDOWEVENT_EXPOSED:
if (!screen_fix_hidpi(screen)) {
// unrecoverable
return false;
}
screen_render(screen);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
@ -626,8 +559,6 @@ screen_handle_window_event(struct screen *screen,
apply_windowed_size(screen);
break;
}
return true;
}
struct point

View File

@ -13,11 +13,6 @@
struct video_buffer;
struct scale_ratio {
int window;
int drawable;
};
struct screen {
SDL_Window *window;
SDL_Renderer *renderer;
@ -38,11 +33,6 @@ struct screen {
bool maximized;
bool no_window;
bool mipmaps;
struct {
struct scale_ratio w;
struct scale_ratio h;
} scale;
};
#define SCREEN_INITIALIZER { \
@ -73,16 +63,6 @@ struct screen {
.maximized = false, \
.no_window = false, \
.mipmaps = false, \
.scale = { \
.w = { \
.window = 0, \
.drawable = 0, \
}, \
.h = { \
.window = 0, \
.drawable = 0, \
}, \
} \
}
// initialize default values
@ -131,8 +111,7 @@ void
screen_set_rotation(struct screen *screen, unsigned rotation);
// react to window events
// return true on success, false on unrecoverable error
bool
void
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
// convert point from window coordinates to frame coordinates

View File

@ -231,22 +231,17 @@ enable_tunnel_any_port(struct server *server, struct port_range port_range) {
static process_t
execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
char lock_video_orientation_string[3];
char display_id_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu16, params->display_id);
const char *const cmd[] = {
"shell",
"CLASSPATH=" DEVICE_SERVER_PATH,
"app_process",
process_t result = PROCESS_NONE;
char *cmd[128];
int i = 0;
cmd[i++] = "shell";
cmd[i++] = "CLASSPATH=" DEVICE_SERVER_PATH;
cmd[i++] = "app_process";
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
cmd[i++] =
# ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
@ -254,21 +249,38 @@ execute_server(struct server *server, const struct server_params *params) {
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
SERVER_DEBUGGER_PORT,
SERVER_DEBUGGER_PORT;
#endif
"/", // unused
"com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
max_size_string,
bit_rate_string,
max_fps_string,
lock_video_orientation_string,
server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
display_id_string,
};
cmd[i++] = "/"; // unused
cmd[i++] = "com.genymobile.scrcpy.Server";
cmd[i++] = SCRCPY_VERSION;
int dyn_index = i; // from there, the strings are allocated
#define ADD_PARAM(fmt, ...) \
cmd[i] = sc_asprintf(fmt, ## __VA_ARGS__); \
if (!cmd[i++]) { \
goto end; \
}
#define STRBOOL(p) (p ? "true" : "false")
ADD_PARAM("max_size=%"PRIu16, params->max_size);
ADD_PARAM("bit_rate=%"PRIu32, params->bit_rate);
ADD_PARAM("max_fps=%"PRIu16, params->max_fps);
ADD_PARAM("lock_video_orientation=%"PRIi8, params->lock_video_orientation);
ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel_forward));
ADD_PARAM("crop=%s", params->crop ? params->crop : "");
// always send frame meta (packet boundaries + timestamp)
ADD_PARAM("send_frame_meta=true");
ADD_PARAM("control=%s", STRBOOL(params->control));
ADD_PARAM("display_id=%"PRIu16, params->display_id);
ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches));
ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake));
#undef ADD_PARAM
#undef STRBOOL
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
@ -280,7 +292,14 @@ execute_server(struct server *server, const struct server_params *params) {
// Port: 5005
// Then click on "Debug"
#endif
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
result = adb_execute(server->serial, (const char **) cmd, i);
end:
for (int j = i; j > dyn_index; --j) {
free(cmd[j - 1]);
}
return result;
}
static socket_t

View File

@ -51,6 +51,8 @@ struct server_params {
int8_t lock_video_orientation;
bool control;
uint16_t display_id;
bool show_touches;
bool stay_awake;
};
// init default values

View File

@ -1,7 +1,9 @@
#include "str_util.h"
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
@ -195,3 +197,33 @@ utf8_from_wide_char(const wchar_t *ws) {
}
#endif
char *
sc_asprintf(const char *fmt, ...) {
va_list va;
va_start(va, fmt);
char *s = sc_vasprintf(fmt, va);
va_end(va);
return s;
}
char *
sc_vasprintf(const char *fmt, va_list ap) {
va_list va;
va_copy(va, ap);
int len = vsnprintf(NULL, 0, fmt, va);
va_end(va);
char *str = malloc(len + 1);
if (!str) {
return NULL;
}
va_copy(va, ap);
int len2 = vsprintf(str, fmt, va);
(void) len2;
assert(len == len2);
va_end(va);
return str;
}

View File

@ -1,8 +1,10 @@
#ifndef STRUTIL_H
#define STRUTIL_H
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include "config.h"
@ -57,4 +59,13 @@ char *
utf8_from_wide_char(const wchar_t *s);
#endif
// compatibility function similar to asprintf()
// (but returning the resulting string for convenience)
char *
sc_asprintf(const char *fmt, ...)
__attribute__((format(printf, 1, 2)));
char *
sc_vasprintf(const char *fmt, va_list ap);
#endif

View File

@ -15,6 +15,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32'
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32'

View File

@ -15,6 +15,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32'

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '1.12.1',
version: '1.13',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',

View File

@ -4,5 +4,6 @@ option('crossbuild_windows', type: 'boolean', value: false, description: 'Build
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')

View File

@ -10,29 +10,29 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \
9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \
ffmpeg-4.2.1-win32-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \
ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \
ffmpeg-4.2.2-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \
c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \
ffmpeg-4.2.1-win32-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \
8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \
ffmpeg-4.2.2-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \
55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \
ffmpeg-4.2.1-win64-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \
5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \
ffmpeg-4.2.2-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \
5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \
ffmpeg-4.2.1-win64-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \
f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \
ffmpeg-4.2.2-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \
SDL2-2.0.10
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \
e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \
SDL2-2.0.12
prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \

View File

@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 29
versionCode 14
versionName "1.12.1"
versionCode 15
versionName "1.13"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.12.1
SCRCPY_VERSION_NAME=1.13
PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}

View File

@ -0,0 +1,77 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import java.io.File;
import java.io.IOException;
/**
* Handle the cleanup of scrcpy, even if the main process is killed.
* <p>
* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process).
*/
public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private CleanUp() {
// not instantiable
}
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
}
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
builder.start();
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
public static void main(String... args) {
unlinkSelf();
try {
// Wait for the server to die
System.in.read();
} catch (IOException e) {
// Expected when the server is dead
}
Ln.i("Cleaning up");
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
if (disableShowTouches || restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
}
if (restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
}
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
@ -199,4 +200,8 @@ public final class Device {
wm.thawRotation();
}
}
public ContentProvider createSettingsProvider() {
return serviceManager.getActivityManager().createSettingsProvider();
}
}

View File

@ -6,12 +6,14 @@ public class Options {
private int maxSize;
private int bitRate;
private int maxFps;
private int lockedVideoOrientation;
private int lockedVideoOrientation = -1;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
private int displayId;
private boolean showTouches;
private boolean stayAwake;
public int getMaxSize() {
return maxSize;
@ -84,4 +86,20 @@ public class Options {
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public boolean getShowTouches() {
return showTouches;
}
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() {
return stayAwake;
}
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
}

View File

@ -47,9 +47,21 @@ public class ScreenEncoder implements Device.RotationListener {
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
Workarounds.fillAppInfo();
MediaFormat format = createFormat(bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
try {
internalStreamScreen(device, fd);
} catch (NullPointerException e) {
// Retry with workarounds enabled:
// <https://github.com/Genymobile/scrcpy/issues/365>
// <https://github.com/Genymobile/scrcpy/issues/940>
Ln.d("Applying workarounds to avoid NullPointerException");
Workarounds.fillAppInfo();
internalStreamScreen(device, fd);
}
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps);
device.setRotationListener(this);
boolean alive;
try {
@ -139,14 +151,14 @@ public class ScreenEncoder implements Device.RotationListener {
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
}
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
private static MediaFormat createFormat(int bitRate, int maxFps) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// 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);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
if (maxFps > 0) {

View File

@ -1,15 +1,16 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.os.BatteryManager;
import android.os.Build;
import java.io.File;
import java.io.IOException;
public final class Server {
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private Server() {
// not instantiable
@ -18,6 +19,35 @@ public final class Server {
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1;
if (options.getShowTouches() || options.getStayAwake()) {
try (ContentProvider settings = device.createSettingsProvider()) {
if (options.getShowTouches()) {
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) {
// No need to restore
restoreStayOn = -1;
}
} catch (NumberFormatException e) {
restoreStayOn = 0;
}
}
}
}
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
@ -79,45 +109,73 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
if (args.length != 10) {
throw new IllegalArgumentException("Expecting 10 parameters");
}
Options options = new Options();
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
options.setMaxSize(maxSize);
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
int bitRate = Integer.parseInt(args[2]);
options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[4]);
options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[5]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[6]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[8]);
options.setControl(control);
int displayId = Integer.parseInt(args[9]);
options.setDisplayId(displayId);
switch (key) {
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "bit_rate":
int bitRate = Integer.parseInt(value);
options.setBitRate(bitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
options.setMaxFps(maxFps);
break;
case "lock_video_orientation":
int lockedVideoOrientation = Integer.parseInt(value);
options.setLockedVideoOrientation(lockedVideoOrientation);
break;
case "tunnel_forward":
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(value);
options.setTunnelForward(tunnelForward);
break;
case "crop":
Rect crop = parseCrop(value);
options.setCrop(crop);
break;
case "send_frame_meta":
boolean sendFrameMeta = Boolean.parseBoolean(value);
options.setSendFrameMeta(sendFrameMeta);
break;
case "control":
boolean control = Boolean.parseBoolean(value);
options.setControl(control);
break;
case "display_id":
int displayId = Integer.parseInt(value);
options.setDisplayId(displayId);
break;
case "show_touches":
boolean showTouches = Boolean.parseBoolean(value);
options.setShowTouches(showTouches);
break;
case "stay_awake":
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
default:
throw new IllegalArgumentException("Unknown parameter: " + key);
}
}
return options;
}
private static Rect parseCrop(String crop) {
if ("-".equals(crop)) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
@ -132,14 +190,6 @@ public final class Server {
return new Rect(x, y, x + width, y + height);
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
private static void suggestFix(Throwable e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e instanceof MediaCodec.CodecException) {
@ -172,7 +222,6 @@ public final class Server {
}
});
unlinkSelf();
Options options = createOptions(args);
scrcpy(options);
}

View File

@ -0,0 +1,87 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodLegacy;
private Method removeContentProviderExternalMethod;
public ActivityManager(IInterface manager) {
this.manager = manager;
}
private Method getGetContentProviderExternalMethod() throws NoSuchMethodException {
if (getContentProviderExternalMethod == null) {
try {
getContentProviderExternalMethod = manager.getClass()
.getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class);
} catch (NoSuchMethodException e) {
// old version
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
getContentProviderExternalMethodLegacy = true;
}
}
return getContentProviderExternalMethod;
}
private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException {
if (removeContentProviderExternalMethod == null) {
removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class);
}
return removeContentProviderExternalMethod;
}
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (!getContentProviderExternalMethodLegacy) {
// new version
args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else {
// old version
args = new Object[]{name, ServiceManager.USER_ID, token};
}
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
Object providerHolder = method.invoke(manager, args);
if (providerHolder == null) {
return null;
}
// IContentProvider provider = providerHolder.provider;
Field providerField = providerHolder.getClass().getDeclaredField("provider");
providerField.setAccessible(true);
Object provider = providerField.get(providerHolder);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
void removeContentProviderExternal(String name, IBinder token) {
try {
Method method = getRemoveContentProviderExternalMethod();
method.invoke(manager, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder());
}
}

View File

@ -10,10 +10,6 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClipboardManager {
private static final String PACKAGE_NAME = "com.android.shell";
private static final int USER_ID = 0;
private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
@ -46,17 +42,17 @@ public class ClipboardManager {
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, PACKAGE_NAME);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
}
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, PACKAGE_NAME);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
} else {
method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}

View File

@ -0,0 +1,132 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.Bundle;
import android.os.IBinder;
import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ContentProvider implements Closeable {
public static final String TABLE_SYSTEM = "system";
public static final String TABLE_SECURE = "secure";
public static final String TABLE_GLOBAL = "global";
// See android/providerHolder/Settings.java
private static final String CALL_METHOD_GET_SYSTEM = "GET_system";
private static final String CALL_METHOD_GET_SECURE = "GET_secure";
private static final String CALL_METHOD_GET_GLOBAL = "GET_global";
private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
private static final String CALL_METHOD_PUT_SECURE = "PUT_secure";
private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global";
private static final String CALL_METHOD_USER_KEY = "_user";
private static final String NAME_VALUE_TABLE_VALUE = "value";
private final ActivityManager manager;
// android.content.IContentProvider
private final Object provider;
private final String name;
private final IBinder token;
private Method callMethod;
private boolean callMethodLegacy;
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
this.manager = manager;
this.provider = provider;
this.name = name;
this.token = token;
}
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
try {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
} catch (NoSuchMethodException e) {
// old version
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
callMethodLegacy = true;
}
}
return callMethod;
}
private Bundle call(String callMethod, String arg, Bundle extras) {
try {
Method method = getCallMethod();
Object[] args;
if (!callMethodLegacy) {
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
} else {
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
}
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
public void close() {
manager.removeContentProviderExternal(name, token);
}
private static String getGetMethod(String table) {
switch (table) {
case TABLE_SECURE:
return CALL_METHOD_GET_SECURE;
case TABLE_SYSTEM:
return CALL_METHOD_GET_SYSTEM;
case TABLE_GLOBAL:
return CALL_METHOD_GET_GLOBAL;
default:
throw new IllegalArgumentException("Invalid table: " + table);
}
}
private static String getPutMethod(String table) {
switch (table) {
case TABLE_SECURE:
return CALL_METHOD_PUT_SECURE;
case TABLE_SYSTEM:
return CALL_METHOD_PUT_SYSTEM;
case TABLE_GLOBAL:
return CALL_METHOD_PUT_GLOBAL;
default:
throw new IllegalArgumentException("Invalid table: " + table);
}
}
public String getValue(String table, String key) {
String method = getGetMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
}
return bundle.getString("value");
}
public void putValue(String table, String key, String value) {
String method = getPutMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putString(NAME_VALUE_TABLE_VALUE, value);
call(method, key, arg);
}
public String getAndPutValue(String table, String key, String value) {
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
}
return oldValue;
}
}

View File

@ -8,6 +8,10 @@ import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0;
private final Method getServiceMethod;
private WindowManager windowManager;
@ -16,6 +20,7 @@ public final class ServiceManager {
private PowerManager powerManager;
private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
private ActivityManager activityManager;
public ServiceManager() {
try {
@ -76,4 +81,21 @@ public final class ServiceManager {
}
return clipboardManager;
}
public ActivityManager getActivityManager() {
if (activityManager == null) {
try {
// On old Android versions, the ActivityManager is not exposed via AIDL,
// so use ActivityManagerNative.getDefault()
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
IInterface am = (IInterface) getDefaultMethod.invoke(null);
activityManager = new ActivityManager(am);
} catch (Exception e) {
throw new AssertionError(e);
}
}
return activityManager;
}
}