Compare commits
16 Commits
seamless_c
...
textpaste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d613b10efc | ||
|
|
cc4e1e20de | ||
|
|
4bbabfb4ef | ||
|
|
ffc57512b3 | ||
|
|
c7a33fac36 | ||
|
|
81573d81a0 | ||
|
|
5c2cf88a1d | ||
|
|
8f46e18426 | ||
|
|
ee3882f8be | ||
|
|
a3ef461d73 | ||
|
|
3df63c579d | ||
|
|
56bff2f718 | ||
|
|
080a4ee365 | ||
|
|
c7155a1954 | ||
|
|
517dbd9c85 | ||
|
|
acc4ef31df |
19
README.md
19
README.md
@@ -289,6 +289,22 @@ From another terminal:
|
|||||||
scrcpy
|
scrcpy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To avoid enabling remote port forwarding, you could force a forward connection
|
||||||
|
instead (notice the `-L` instead of `-R`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb kill-server # kill the local adb server on 5037
|
||||||
|
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||||
|
# keep this open
|
||||||
|
```
|
||||||
|
|
||||||
|
From another terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --force-adb-forwrad
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Like for wireless connections, it may be useful to reduce quality:
|
Like for wireless connections, it may be useful to reduce quality:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -482,6 +498,9 @@ both directions:
|
|||||||
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||||
breaks non-ASCII characters).
|
breaks non-ASCII characters).
|
||||||
|
|
||||||
|
Moreover, any time the Android clipboard changes, it is automatically
|
||||||
|
synchronized to the computer clipboard.
|
||||||
|
|
||||||
#### Text injection preference
|
#### Text injection preference
|
||||||
|
|
||||||
There are two kinds of [events][textevents] generated when typing text:
|
There are two kinds of [events][textevents] generated when typing text:
|
||||||
|
|||||||
22
app/scrcpy.1
22
app/scrcpy.1
@@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
|||||||
|
|
||||||
Default is 8000000.
|
Default is 8000000.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-codec\-options " key[:type]=value[,...]
|
||||||
|
Set a list of comma-separated key:type=value options for the device encoder.
|
||||||
|
|
||||||
|
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||||
|
|
||||||
|
The list of possible codec options is available in the Android documentation
|
||||||
|
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||||
|
.UE .
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||||
Crop the device screen on the server.
|
Crop the device screen on the server.
|
||||||
@@ -42,6 +52,10 @@ The list of possible display ids can be listed by "adb shell dumpsys display"
|
|||||||
|
|
||||||
Default is 0.
|
Default is 0.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-force\-adb\-forward
|
||||||
|
Do not attempt to use "adb reverse" to connect to the device.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-f, \-\-fullscreen
|
.B \-f, \-\-fullscreen
|
||||||
Start in fullscreen.
|
Start in fullscreen.
|
||||||
@@ -136,7 +150,7 @@ Turn the device screen off immediately.
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-t, \-\-show\-touches
|
.B \-t, \-\-show\-touches
|
||||||
Enable "show touches" on start, restore the initial value on exit..
|
Enable "show touches" on start, restore the initial value on exit.
|
||||||
|
|
||||||
It only shows physical touches (not clicks from scrcpy).
|
It only shows physical touches (not clicks from scrcpy).
|
||||||
|
|
||||||
@@ -144,6 +158,12 @@ It only shows physical touches (not clicks from scrcpy).
|
|||||||
.B \-v, \-\-version
|
.B \-v, \-\-version
|
||||||
Print the version of scrcpy.
|
Print the version of scrcpy.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-V, \-\-verbosity " value
|
||||||
|
Set the log level ("debug", "info", "warn" or "error").
|
||||||
|
|
||||||
|
Default is "info" for release builds, "debug" for debug builds.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-w, \-\-stay-awake
|
.B \-w, \-\-stay-awake
|
||||||
Keep the device on while scrcpy is running.
|
Keep the device on while scrcpy is running.
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
" Default is %d.\n"
|
" Default is %d.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --codec-options key[:type]=value[,...]\n"
|
||||||
|
" Set a list of comma-separated key:type=value options for the\n"
|
||||||
|
" device encoder.\n"
|
||||||
|
" The possible values for 'type' are 'int' (default), 'long',\n"
|
||||||
|
" 'float' and 'string'.\n"
|
||||||
|
" The list of possible codec options is available in the\n"
|
||||||
|
" Android documentation:\n"
|
||||||
|
" <https://d.android.com/reference/android/media/MediaFormat>\n"
|
||||||
|
"\n"
|
||||||
" --crop width:height:x:y\n"
|
" --crop width:height:x:y\n"
|
||||||
" Crop the device screen on the server.\n"
|
" Crop the device screen on the server.\n"
|
||||||
" The values are expressed in the device natural orientation\n"
|
" The values are expressed in the device natural orientation\n"
|
||||||
@@ -45,6 +54,10 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" Default is 0.\n"
|
" Default is 0.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --force-adb-forward\n"
|
||||||
|
" Do not attempt to use \"adb reverse\" to connect to the\n"
|
||||||
|
" the device.\n"
|
||||||
|
"\n"
|
||||||
" -f, --fullscreen\n"
|
" -f, --fullscreen\n"
|
||||||
" Start in fullscreen.\n"
|
" Start in fullscreen.\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -136,6 +149,14 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" -v, --version\n"
|
" -v, --version\n"
|
||||||
" Print the version of scrcpy.\n"
|
" Print the version of scrcpy.\n"
|
||||||
|
"\n"
|
||||||
|
" -V, --verbosity value\n"
|
||||||
|
" Set the log level (debug, info, warn or error).\n"
|
||||||
|
#ifndef NDEBUG
|
||||||
|
" Default is debug.\n"
|
||||||
|
#else
|
||||||
|
" Default is info.\n"
|
||||||
|
#endif
|
||||||
"\n"
|
"\n"
|
||||||
" -w, --stay-awake\n"
|
" -w, --stay-awake\n"
|
||||||
" Keep the device on while scrcpy is running.\n"
|
" Keep the device on while scrcpy is running.\n"
|
||||||
@@ -424,6 +445,32 @@ parse_display_id(const char *s, uint16_t *display_id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_log_level(const char *s, enum sc_log_level *log_level) {
|
||||||
|
if (!strcmp(s, "debug")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_DEBUG;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "info")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_INFO;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "warn")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_WARN;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "error")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_ERROR;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGE("Could not parse log level: %s", s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_record_format(const char *optarg, enum recorder_format *format) {
|
parse_record_format(const char *optarg, enum recorder_format *format) {
|
||||||
if (!strcmp(optarg, "mp4")) {
|
if (!strcmp(optarg, "mp4")) {
|
||||||
@@ -472,14 +519,19 @@ guess_record_format(const char *filename) {
|
|||||||
#define OPT_ROTATION 1015
|
#define OPT_ROTATION 1015
|
||||||
#define OPT_RENDER_DRIVER 1016
|
#define OPT_RENDER_DRIVER 1016
|
||||||
#define OPT_NO_MIPMAPS 1017
|
#define OPT_NO_MIPMAPS 1017
|
||||||
|
#define OPT_CODEC_OPTIONS 1018
|
||||||
|
#define OPT_FORCE_ADB_FORWARD 1019
|
||||||
|
|
||||||
bool
|
bool
|
||||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
static const struct option long_options[] = {
|
static const struct option long_options[] = {
|
||||||
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
||||||
{"bit-rate", required_argument, NULL, 'b'},
|
{"bit-rate", required_argument, NULL, 'b'},
|
||||||
|
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
|
||||||
{"crop", required_argument, NULL, OPT_CROP},
|
{"crop", required_argument, NULL, OPT_CROP},
|
||||||
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
||||||
|
{"force-adb-forward", no_argument, NULL,
|
||||||
|
OPT_FORCE_ADB_FORWARD},
|
||||||
{"fullscreen", no_argument, NULL, 'f'},
|
{"fullscreen", no_argument, NULL, 'f'},
|
||||||
{"help", no_argument, NULL, 'h'},
|
{"help", no_argument, NULL, 'h'},
|
||||||
{"lock-video-orientation", required_argument, NULL,
|
{"lock-video-orientation", required_argument, NULL,
|
||||||
@@ -502,6 +554,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
{"show-touches", no_argument, NULL, 't'},
|
{"show-touches", no_argument, NULL, 't'},
|
||||||
{"stay-awake", no_argument, NULL, 'w'},
|
{"stay-awake", no_argument, NULL, 'w'},
|
||||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||||
|
{"verbosity", required_argument, NULL, 'V'},
|
||||||
{"version", no_argument, NULL, 'v'},
|
{"version", no_argument, NULL, 'v'},
|
||||||
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
||||||
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
||||||
@@ -518,8 +571,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
optind = 0; // reset to start from the first argument in tests
|
optind = 0; // reset to start from the first argument in tests
|
||||||
|
|
||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options,
|
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
|
||||||
NULL)) != -1) {
|
long_options, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'b':
|
case 'b':
|
||||||
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
||||||
@@ -598,6 +651,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case 'v':
|
case 'v':
|
||||||
args->version = true;
|
args->version = true;
|
||||||
break;
|
break;
|
||||||
|
case 'V':
|
||||||
|
if (!parse_log_level(optarg, &opts->log_level)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'w':
|
case 'w':
|
||||||
opts->stay_awake = true;
|
opts->stay_awake = true;
|
||||||
break;
|
break;
|
||||||
@@ -647,6 +705,12 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case OPT_NO_MIPMAPS:
|
case OPT_NO_MIPMAPS:
|
||||||
opts->mipmaps = false;
|
opts->mipmaps = false;
|
||||||
break;
|
break;
|
||||||
|
case OPT_CODEC_OPTIONS:
|
||||||
|
opts->codec_options = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_FORCE_ADB_FORWARD:
|
||||||
|
opts->force_adb_forward = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ print_version(void) {
|
|||||||
LIBAVUTIL_VERSION_MICRO);
|
LIBAVUTIL_VERSION_MICRO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SDL_LogPriority
|
||||||
|
convert_log_level_to_sdl(enum sc_log_level level) {
|
||||||
|
switch (level) {
|
||||||
|
case SC_LOG_LEVEL_DEBUG:
|
||||||
|
return SDL_LOG_PRIORITY_DEBUG;
|
||||||
|
case SC_LOG_LEVEL_INFO:
|
||||||
|
return SDL_LOG_PRIORITY_INFO;
|
||||||
|
case SC_LOG_LEVEL_WARN:
|
||||||
|
return SDL_LOG_PRIORITY_WARN;
|
||||||
|
case SC_LOG_LEVEL_ERROR:
|
||||||
|
return SDL_LOG_PRIORITY_ERROR;
|
||||||
|
default:
|
||||||
|
assert(!"unexpected log level");
|
||||||
|
return SC_LOG_LEVEL_INFO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char *argv[]) {
|
main(int argc, char *argv[]) {
|
||||||
#ifdef __WINDOWS__
|
#ifdef __WINDOWS__
|
||||||
@@ -38,20 +56,23 @@ main(int argc, char *argv[]) {
|
|||||||
setbuf(stderr, NULL);
|
setbuf(stderr, NULL);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef NDEBUG
|
|
||||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct scrcpy_cli_args args = {
|
struct scrcpy_cli_args args = {
|
||||||
.opts = SCRCPY_OPTIONS_DEFAULT,
|
.opts = SCRCPY_OPTIONS_DEFAULT,
|
||||||
.help = false,
|
.help = false,
|
||||||
.version = false,
|
.version = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
args.opts.log_level = SC_LOG_LEVEL_DEBUG;
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!scrcpy_parse_args(&args, argc, argv)) {
|
if (!scrcpy_parse_args(&args, argc, argv)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level);
|
||||||
|
SDL_LogSetAllPriority(sdl_log);
|
||||||
|
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
scrcpy_print_usage(argv[0]);
|
scrcpy_print_usage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ bool
|
|||||||
scrcpy(const struct scrcpy_options *options) {
|
scrcpy(const struct scrcpy_options *options) {
|
||||||
bool record = !!options->record_filename;
|
bool record = !!options->record_filename;
|
||||||
struct server_params params = {
|
struct server_params params = {
|
||||||
|
.log_level = options->log_level,
|
||||||
.crop = options->crop,
|
.crop = options->crop,
|
||||||
.port_range = options->port_range,
|
.port_range = options->port_range,
|
||||||
.max_size = options->max_size,
|
.max_size = options->max_size,
|
||||||
@@ -303,6 +304,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
.display_id = options->display_id,
|
.display_id = options->display_id,
|
||||||
.show_touches = options->show_touches,
|
.show_touches = options->show_touches,
|
||||||
.stay_awake = options->stay_awake,
|
.stay_awake = options->stay_awake,
|
||||||
|
.codec_options = options->codec_options,
|
||||||
|
.force_adb_forward = options->force_adb_forward,
|
||||||
};
|
};
|
||||||
if (!server_start(&server, options->serial, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
struct scrcpy_options {
|
struct scrcpy_options {
|
||||||
const char *serial;
|
const char *serial;
|
||||||
@@ -16,6 +17,8 @@ struct scrcpy_options {
|
|||||||
const char *window_title;
|
const char *window_title;
|
||||||
const char *push_target;
|
const char *push_target;
|
||||||
const char *render_driver;
|
const char *render_driver;
|
||||||
|
const char *codec_options;
|
||||||
|
enum sc_log_level log_level;
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
struct port_range port_range;
|
struct port_range port_range;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
@@ -39,6 +42,7 @@ struct scrcpy_options {
|
|||||||
bool window_borderless;
|
bool window_borderless;
|
||||||
bool mipmaps;
|
bool mipmaps;
|
||||||
bool stay_awake;
|
bool stay_awake;
|
||||||
|
bool force_adb_forward;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCRCPY_OPTIONS_DEFAULT { \
|
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||||
@@ -48,6 +52,8 @@ struct scrcpy_options {
|
|||||||
.window_title = NULL, \
|
.window_title = NULL, \
|
||||||
.push_target = NULL, \
|
.push_target = NULL, \
|
||||||
.render_driver = NULL, \
|
.render_driver = NULL, \
|
||||||
|
.codec_options = NULL, \
|
||||||
|
.log_level = SC_LOG_LEVEL_INFO, \
|
||||||
.record_format = RECORDER_FORMAT_AUTO, \
|
.record_format = RECORDER_FORMAT_AUTO, \
|
||||||
.port_range = { \
|
.port_range = { \
|
||||||
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
||||||
@@ -74,6 +80,7 @@ struct scrcpy_options {
|
|||||||
.window_borderless = false, \
|
.window_borderless = false, \
|
||||||
.mipmaps = true, \
|
.mipmaps = true, \
|
||||||
.stay_awake = false, \
|
.stay_awake = false, \
|
||||||
|
.force_adb_forward = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
|||||||
@@ -217,24 +217,46 @@ enable_tunnel_forward_any_port(struct server *server,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
|
enable_tunnel_any_port(struct server *server, struct port_range port_range,
|
||||||
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
bool force_adb_forward) {
|
||||||
return true;
|
if (!force_adb_forward) {
|
||||||
|
// Attempt to use "adb reverse"
|
||||||
|
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if "adb reverse" does not work (e.g. over "adb connect"), it
|
||||||
|
// fallbacks to "adb forward", so the app socket is the client
|
||||||
|
|
||||||
|
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
|
|
||||||
// "adb forward", so the app socket is the client
|
|
||||||
|
|
||||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
|
||||||
return enable_tunnel_forward_any_port(server, port_range);
|
return enable_tunnel_forward_any_port(server, port_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
log_level_to_server_string(enum sc_log_level level) {
|
||||||
|
switch (level) {
|
||||||
|
case SC_LOG_LEVEL_DEBUG:
|
||||||
|
return "debug";
|
||||||
|
case SC_LOG_LEVEL_INFO:
|
||||||
|
return "info";
|
||||||
|
case SC_LOG_LEVEL_WARN:
|
||||||
|
return "warn";
|
||||||
|
case SC_LOG_LEVEL_ERROR:
|
||||||
|
return "error";
|
||||||
|
default:
|
||||||
|
assert(!"unexpected log level");
|
||||||
|
return "(unknown)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static process_t
|
static process_t
|
||||||
execute_server(struct server *server, const struct server_params *params) {
|
execute_server(struct server *server, const struct server_params *params) {
|
||||||
char max_size_string[6];
|
char max_size_string[6];
|
||||||
char bit_rate_string[11];
|
char bit_rate_string[11];
|
||||||
char max_fps_string[6];
|
char max_fps_string[6];
|
||||||
char lock_video_orientation_string[3];
|
char lock_video_orientation_string[5];
|
||||||
char display_id_string[6];
|
char display_id_string[6];
|
||||||
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
||||||
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
||||||
@@ -259,6 +281,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
"/", // unused
|
"/", // unused
|
||||||
"com.genymobile.scrcpy.Server",
|
"com.genymobile.scrcpy.Server",
|
||||||
SCRCPY_VERSION,
|
SCRCPY_VERSION,
|
||||||
|
log_level_to_server_string(params->log_level),
|
||||||
max_size_string,
|
max_size_string,
|
||||||
bit_rate_string,
|
bit_rate_string,
|
||||||
max_fps_string,
|
max_fps_string,
|
||||||
@@ -270,6 +293,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
display_id_string,
|
display_id_string,
|
||||||
params->show_touches ? "true" : "false",
|
params->show_touches ? "true" : "false",
|
||||||
params->stay_awake ? "true" : "false",
|
params->stay_awake ? "true" : "false",
|
||||||
|
params->codec_options ? params->codec_options : "-",
|
||||||
};
|
};
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
LOGI("Server debugger waiting for a client on device port "
|
LOGI("Server debugger waiting for a client on device port "
|
||||||
@@ -365,7 +389,8 @@ server_start(struct server *server, const char *serial,
|
|||||||
goto error1;
|
goto error1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enable_tunnel_any_port(server, params->port_range)) {
|
if (!enable_tunnel_any_port(server, params->port_range,
|
||||||
|
params->force_adb_forward)) {
|
||||||
goto error1;
|
goto error1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
struct server {
|
struct server {
|
||||||
@@ -43,7 +44,9 @@ struct server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct server_params {
|
struct server_params {
|
||||||
|
enum sc_log_level log_level;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
|
const char *codec_options;
|
||||||
struct port_range port_range;
|
struct port_range port_range;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t bit_rate;
|
||||||
@@ -53,6 +56,7 @@ struct server_params {
|
|||||||
uint16_t display_id;
|
uint16_t display_id;
|
||||||
bool show_touches;
|
bool show_touches;
|
||||||
bool stay_awake;
|
bool stay_awake;
|
||||||
|
bool force_adb_forward;
|
||||||
};
|
};
|
||||||
|
|
||||||
// init default values
|
// init default values
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
|
|
||||||
#include <SDL2/SDL_log.h>
|
#include <SDL2/SDL_log.h>
|
||||||
|
|
||||||
|
enum sc_log_level {
|
||||||
|
SC_LOG_LEVEL_DEBUG,
|
||||||
|
SC_LOG_LEVEL_INFO,
|
||||||
|
SC_LOG_LEVEL_WARN,
|
||||||
|
SC_LOG_LEVEL_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2008, The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@hide}
|
||||||
|
*/
|
||||||
|
oneway interface IOnPrimaryClipChangedListener {
|
||||||
|
void dispatchPrimaryClipChanged();
|
||||||
|
}
|
||||||
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CodecOption {
|
||||||
|
private String key;
|
||||||
|
private Object value;
|
||||||
|
|
||||||
|
public CodecOption(String key, Object value) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CodecOption> parse(String codecOptions) {
|
||||||
|
if ("-".equals(codecOptions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CodecOption> result = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean escape = false;
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
|
||||||
|
for (char c : codecOptions.toCharArray()) {
|
||||||
|
switch (c) {
|
||||||
|
case '\\':
|
||||||
|
if (escape) {
|
||||||
|
buf.append('\\');
|
||||||
|
escape = false;
|
||||||
|
} else {
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ',':
|
||||||
|
if (escape) {
|
||||||
|
buf.append(',');
|
||||||
|
escape = false;
|
||||||
|
} else {
|
||||||
|
// This comma is a separator between codec options
|
||||||
|
String codecOption = buf.toString();
|
||||||
|
result.add(parseOption(codecOption));
|
||||||
|
// Clear buf
|
||||||
|
buf.setLength(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
buf.append(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.length() > 0) {
|
||||||
|
String codecOption = buf.toString();
|
||||||
|
result.add(parseOption(codecOption));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CodecOption parseOption(String option) {
|
||||||
|
int equalSignIndex = option.indexOf('=');
|
||||||
|
if (equalSignIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("'=' expected");
|
||||||
|
}
|
||||||
|
String keyAndType = option.substring(0, equalSignIndex);
|
||||||
|
if (keyAndType.length() == 0) {
|
||||||
|
throw new IllegalArgumentException("Key may not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String key;
|
||||||
|
String type;
|
||||||
|
|
||||||
|
int colonIndex = keyAndType.indexOf(':');
|
||||||
|
if (colonIndex != -1) {
|
||||||
|
key = keyAndType.substring(0, colonIndex);
|
||||||
|
type = keyAndType.substring(colonIndex + 1);
|
||||||
|
} else {
|
||||||
|
key = keyAndType;
|
||||||
|
type = "int"; // assume int by default
|
||||||
|
}
|
||||||
|
|
||||||
|
Object value;
|
||||||
|
String valueString = option.substring(equalSignIndex + 1);
|
||||||
|
switch (type) {
|
||||||
|
case "int":
|
||||||
|
value = Integer.parseInt(valueString);
|
||||||
|
break;
|
||||||
|
case "long":
|
||||||
|
value = Long.parseLong(valueString);
|
||||||
|
break;
|
||||||
|
case "float":
|
||||||
|
value = Float.parseFloat(valueString);
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
value = valueString;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CodecOption(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ public class ControlMessageReader {
|
|||||||
|
|
||||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
||||||
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
||||||
private static final int RAW_BUFFER_SIZE = 1024;
|
|
||||||
|
private static final int RAW_BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
||||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
|
||||||
import android.view.KeyCharacterMap;
|
import android.view.KeyCharacterMap;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -50,7 +47,7 @@ public class Controller {
|
|||||||
public void control() throws IOException {
|
public void control() throws IOException {
|
||||||
// on start, power on the device
|
// on start, power on the device
|
||||||
if (!device.isScreenOn()) {
|
if (!device.isScreenOn()) {
|
||||||
injectKeycode(KeyEvent.KEYCODE_POWER);
|
device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||||
|
|
||||||
// dirty hack
|
// dirty hack
|
||||||
// After POWER is injected, the device is powered on asynchronously.
|
// After POWER is injected, the device is powered on asynchronously.
|
||||||
@@ -110,11 +107,18 @@ public class Controller {
|
|||||||
sender.pushClipboardText(clipboardText);
|
sender.pushClipboardText(clipboardText);
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
device.setClipboardText(msg.getText());
|
boolean setClipboardOk = device.setClipboardText(msg.getText());
|
||||||
|
if (setClipboardOk) {
|
||||||
|
Ln.i("Device clipboard set");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
device.setScreenPowerMode(msg.getAction());
|
int mode = msg.getAction();
|
||||||
|
boolean setPowerModeOk = device.setScreenPowerMode(mode);
|
||||||
|
if (setPowerModeOk) {
|
||||||
|
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
@@ -126,7 +130,7 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectKeycode(int action, int keycode, int metaState) {
|
private boolean injectKeycode(int action, int keycode, int metaState) {
|
||||||
return injectKeyEvent(action, keycode, 0, metaState);
|
return device.injectKeyEvent(action, keycode, 0, metaState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectChar(char c) {
|
private boolean injectChar(char c) {
|
||||||
@@ -137,7 +141,7 @@ public class Controller {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (KeyEvent event : events) {
|
for (KeyEvent event : events) {
|
||||||
if (!injectEvent(event)) {
|
if (!device.injectEvent(event)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +149,10 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int injectText(String text) {
|
private int injectText(String text) {
|
||||||
|
if (device.injectTextPaste(text)) {
|
||||||
|
// The best method (fastest and UTF-8) worked!
|
||||||
|
return text.length();
|
||||||
|
}
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
for (char c : text.toCharArray()) {
|
for (char c : text.toCharArray()) {
|
||||||
if (!injectChar(c)) {
|
if (!injectChar(c)) {
|
||||||
@@ -193,7 +201,7 @@ public class Controller {
|
|||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||||
return injectEvent(event);
|
return device.injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||||
@@ -216,26 +224,11 @@ public class Controller {
|
|||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||||
return injectEvent(event);
|
return device.injectEvent(event);
|
||||||
}
|
|
||||||
|
|
||||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
|
||||||
long now = SystemClock.uptimeMillis();
|
|
||||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
|
||||||
InputDevice.SOURCE_KEYBOARD);
|
|
||||||
return injectEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean injectKeycode(int keyCode) {
|
|
||||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean injectEvent(InputEvent event) {
|
|
||||||
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pressBackOrTurnScreenOn() {
|
private boolean pressBackOrTurnScreenOn() {
|
||||||
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
||||||
return injectKeycode(keycode);
|
return device.injectKeycode(keycode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||||
|
|
||||||
|
import android.content.IOnPrimaryClipChangedListener;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.SystemClock;
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
|
import android.view.KeyCharacterMap;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class Device {
|
public final class Device {
|
||||||
|
|
||||||
@@ -22,10 +28,16 @@ public final class Device {
|
|||||||
void onRotationChanged(int rotation);
|
void onRotationChanged(int rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ClipboardListener {
|
||||||
|
void onClipboardTextChanged(String text);
|
||||||
|
}
|
||||||
|
|
||||||
private final ServiceManager serviceManager = new ServiceManager();
|
private final ServiceManager serviceManager = new ServiceManager();
|
||||||
|
|
||||||
private ScreenInfo screenInfo;
|
private ScreenInfo screenInfo;
|
||||||
private RotationListener rotationListener;
|
private RotationListener rotationListener;
|
||||||
|
private ClipboardListener clipboardListener;
|
||||||
|
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logical display identifier
|
* Logical display identifier
|
||||||
@@ -66,6 +78,27 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
}, displayId);
|
}, displayId);
|
||||||
|
|
||||||
|
if (options.getControl()) {
|
||||||
|
// If control is enabled, synchronize Android clipboard to the computer automatically
|
||||||
|
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||||
|
@Override
|
||||||
|
public void dispatchPrimaryClipChanged() {
|
||||||
|
if (isSettingClipboard.get()) {
|
||||||
|
// This is a notification for the change we are currently applying, ignore it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (Device.this) {
|
||||||
|
if (clipboardListener != null) {
|
||||||
|
String text = getClipboardText();
|
||||||
|
if (text != null) {
|
||||||
|
clipboardListener.onClipboardTextChanged(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||||
}
|
}
|
||||||
@@ -118,7 +151,7 @@ public final class Device {
|
|||||||
return supportsInputEvents;
|
return supportsInputEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
public boolean injectEvent(InputEvent inputEvent, int mode) {
|
||||||
if (!supportsInputEvents()) {
|
if (!supportsInputEvents()) {
|
||||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||||
}
|
}
|
||||||
@@ -130,6 +163,50 @@ public final class Device {
|
|||||||
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean injectEvent(InputEvent event) {
|
||||||
|
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int mode) {
|
||||||
|
long now = SystemClock.uptimeMillis();
|
||||||
|
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||||
|
InputDevice.SOURCE_KEYBOARD);
|
||||||
|
return injectEvent(event, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||||
|
return injectKeyEvent(action, keyCode, repeat, metaState, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectKeycode(int keyCode, int mode) {
|
||||||
|
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectKeycode(int keyCode) {
|
||||||
|
return injectKeycode(keyCode, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectTextPaste(String text) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Android >= 7, we can inject UTF-8 text as follow:
|
||||||
|
// - set the clipboard
|
||||||
|
// - inject the PASTE key event
|
||||||
|
// - restore the clipboard
|
||||||
|
|
||||||
|
String clipboardBackup = getClipboardText();
|
||||||
|
isSettingClipboard.set(true);
|
||||||
|
|
||||||
|
rawSetClipboardText(text);
|
||||||
|
boolean ok = injectKeycode(KeyEvent.KEYCODE_PASTE, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT);
|
||||||
|
rawSetClipboardText(clipboardBackup);
|
||||||
|
|
||||||
|
isSettingClipboard.set(false);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isScreenOn() {
|
public boolean isScreenOn() {
|
||||||
return serviceManager.getPowerManager().isScreenOn();
|
return serviceManager.getPowerManager().isScreenOn();
|
||||||
}
|
}
|
||||||
@@ -138,6 +215,10 @@ public final class Device {
|
|||||||
this.rotationListener = rotationListener;
|
this.rotationListener = rotationListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
|
||||||
|
this.clipboardListener = clipboardListener;
|
||||||
|
}
|
||||||
|
|
||||||
public void expandNotificationPanel() {
|
public void expandNotificationPanel() {
|
||||||
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
||||||
}
|
}
|
||||||
@@ -154,26 +235,27 @@ public final class Device {
|
|||||||
return s.toString();
|
return s.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClipboardText(String text) {
|
private boolean rawSetClipboardText(String text) {
|
||||||
boolean ok = serviceManager.getClipboardManager().setText(text);
|
return serviceManager.getClipboardManager().setText(text);
|
||||||
if (ok) {
|
}
|
||||||
Ln.i("Device clipboard set");
|
|
||||||
}
|
public boolean setClipboardText(String text) {
|
||||||
|
isSettingClipboard.set(true);
|
||||||
|
boolean ok = rawSetClipboardText(text);
|
||||||
|
isSettingClipboard.set(false);
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public void setScreenPowerMode(int mode) {
|
public boolean setScreenPowerMode(int mode) {
|
||||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
Ln.e("Could not get built-in display");
|
Ln.e("Could not get built-in display");
|
||||||
return;
|
return false;
|
||||||
}
|
|
||||||
boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
|
|
||||||
if (ok) {
|
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
|
||||||
}
|
}
|
||||||
|
return SurfaceControl.setDisplayPowerMode(d, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,14 +15,25 @@ public final class Ln {
|
|||||||
DEBUG, INFO, WARN, ERROR
|
DEBUG, INFO, WARN, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
private static Level threshold;
|
||||||
|
|
||||||
private Ln() {
|
private Ln() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the log level.
|
||||||
|
* <p>
|
||||||
|
* Must be called before starting any new thread.
|
||||||
|
*
|
||||||
|
* @param level the log level
|
||||||
|
*/
|
||||||
|
public static void initLogLevel(Level level) {
|
||||||
|
threshold = level;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isEnabled(Level level) {
|
public static boolean isEnabled(Level level) {
|
||||||
return level.ordinal() >= THRESHOLD.ordinal();
|
return level.ordinal() >= threshold.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void d(String message) {
|
public static void d(String message) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
|
||||||
public class Options {
|
public class Options {
|
||||||
|
private Ln.Level logLevel;
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
@@ -14,6 +15,15 @@ public class Options {
|
|||||||
private int displayId;
|
private int displayId;
|
||||||
private boolean showTouches;
|
private boolean showTouches;
|
||||||
private boolean stayAwake;
|
private boolean stayAwake;
|
||||||
|
private String codecOptions;
|
||||||
|
|
||||||
|
public Ln.Level getLogLevel() {
|
||||||
|
return logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogLevel(Ln.Level logLevel) {
|
||||||
|
this.logLevel = logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxSize() {
|
public int getMaxSize() {
|
||||||
return maxSize;
|
return maxSize;
|
||||||
@@ -102,4 +112,12 @@ public class Options {
|
|||||||
public void setStayAwake(boolean stayAwake) {
|
public void setStayAwake(boolean stayAwake) {
|
||||||
this.stayAwake = stayAwake;
|
this.stayAwake = stayAwake;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCodecOptions() {
|
||||||
|
return codecOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCodecOptions(String codecOptions) {
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.view.Surface;
|
|||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class ScreenEncoder implements Device.RotationListener {
|
public class ScreenEncoder implements Device.RotationListener {
|
||||||
@@ -25,15 +26,17 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
|
private List<CodecOption> codecOptions;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private boolean sendFrameMeta;
|
private boolean sendFrameMeta;
|
||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -61,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
MediaFormat format = createFormat(bitRate, maxFps);
|
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
@@ -151,7 +154,24 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(int bitRate, int maxFps) {
|
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||||
|
String key = codecOption.getKey();
|
||||||
|
Object value = codecOption.getValue();
|
||||||
|
|
||||||
|
if (value instanceof Integer) {
|
||||||
|
format.setInteger(key, (Integer) value);
|
||||||
|
} else if (value instanceof Long) {
|
||||||
|
format.setLong(key, (Long) value);
|
||||||
|
} else if (value instanceof Float) {
|
||||||
|
format.setFloat(key, (Float) value);
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
format.setString(key, (String) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
MediaFormat format = new MediaFormat();
|
MediaFormat format = new MediaFormat();
|
||||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||||
@@ -167,6 +187,13 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
||||||
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (codecOptions != null) {
|
||||||
|
for (CodecOption option : codecOptions) {
|
||||||
|
setCodecOption(format, option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import android.os.BatteryManager;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ public final class Server {
|
|||||||
private static void scrcpy(Options options) throws IOException {
|
private static void scrcpy(Options options) throws IOException {
|
||||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
|
||||||
|
|
||||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||||
int restoreStayOn = -1;
|
int restoreStayOn = -1;
|
||||||
@@ -49,15 +52,23 @@ public final class Server {
|
|||||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
||||||
|
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
Controller controller = new Controller(device, connection);
|
final Controller controller = new Controller(device, connection);
|
||||||
|
|
||||||
// asynchronous
|
// asynchronous
|
||||||
startController(controller);
|
startController(controller);
|
||||||
startDeviceMessageSender(controller.getSender());
|
startDeviceMessageSender(controller.getSender());
|
||||||
|
|
||||||
|
device.setClipboardListener(new Device.ClipboardListener() {
|
||||||
|
@Override
|
||||||
|
public void onClipboardTextChanged(String text) {
|
||||||
|
controller.getSender().pushClipboardText(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -109,47 +120,53 @@ public final class Server {
|
|||||||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
final int expectedParameters = 12;
|
final int expectedParameters = 14;
|
||||||
if (args.length != expectedParameters) {
|
if (args.length != expectedParameters) {
|
||||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
|
|
||||||
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
|
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
|
||||||
|
options.setLogLevel(level);
|
||||||
|
|
||||||
|
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
|
||||||
options.setMaxSize(maxSize);
|
options.setMaxSize(maxSize);
|
||||||
|
|
||||||
int bitRate = Integer.parseInt(args[2]);
|
int bitRate = Integer.parseInt(args[3]);
|
||||||
options.setBitRate(bitRate);
|
options.setBitRate(bitRate);
|
||||||
|
|
||||||
int maxFps = Integer.parseInt(args[3]);
|
int maxFps = Integer.parseInt(args[4]);
|
||||||
options.setMaxFps(maxFps);
|
options.setMaxFps(maxFps);
|
||||||
|
|
||||||
int lockedVideoOrientation = Integer.parseInt(args[4]);
|
int lockedVideoOrientation = Integer.parseInt(args[5]);
|
||||||
options.setLockedVideoOrientation(lockedVideoOrientation);
|
options.setLockedVideoOrientation(lockedVideoOrientation);
|
||||||
|
|
||||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||||
boolean tunnelForward = Boolean.parseBoolean(args[5]);
|
boolean tunnelForward = Boolean.parseBoolean(args[6]);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
|
||||||
Rect crop = parseCrop(args[6]);
|
Rect crop = parseCrop(args[7]);
|
||||||
options.setCrop(crop);
|
options.setCrop(crop);
|
||||||
|
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
|
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
|
|
||||||
boolean control = Boolean.parseBoolean(args[8]);
|
boolean control = Boolean.parseBoolean(args[9]);
|
||||||
options.setControl(control);
|
options.setControl(control);
|
||||||
|
|
||||||
int displayId = Integer.parseInt(args[9]);
|
int displayId = Integer.parseInt(args[10]);
|
||||||
options.setDisplayId(displayId);
|
options.setDisplayId(displayId);
|
||||||
|
|
||||||
boolean showTouches = Boolean.parseBoolean(args[10]);
|
boolean showTouches = Boolean.parseBoolean(args[11]);
|
||||||
options.setShowTouches(showTouches);
|
options.setShowTouches(showTouches);
|
||||||
|
|
||||||
boolean stayAwake = Boolean.parseBoolean(args[11]);
|
boolean stayAwake = Boolean.parseBoolean(args[12]);
|
||||||
options.setStayAwake(stayAwake);
|
options.setStayAwake(stayAwake);
|
||||||
|
|
||||||
|
String codecOptions = args[13];
|
||||||
|
options.setCodecOptions(codecOptions);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +219,9 @@ public final class Server {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Options options = createOptions(args);
|
Options options = createOptions(args);
|
||||||
|
|
||||||
|
Ln.initLogLevel(options.getLogLevel());
|
||||||
|
|
||||||
scrcpy(options);
|
scrcpy(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
|
|||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
|
import android.content.IOnPrimaryClipChangedListener;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ public class ClipboardManager {
|
|||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getPrimaryClipMethod;
|
private Method getPrimaryClipMethod;
|
||||||
private Method setPrimaryClipMethod;
|
private Method setPrimaryClipMethod;
|
||||||
|
private Method addPrimaryClipChangedListener;
|
||||||
|
|
||||||
public ClipboardManager(IInterface manager) {
|
public ClipboardManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@@ -81,4 +83,37 @@ public class ClipboardManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||||
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
||||||
|
} else {
|
||||||
|
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||||
|
if (addPrimaryClipChangedListener == null) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||||
|
} else {
|
||||||
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addPrimaryClipChangedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||||
|
try {
|
||||||
|
Method method = getAddPrimaryClipChangedListener();
|
||||||
|
addPrimaryClipChangedListener(method, manager, listener);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CodecOptionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntegerImplicit() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key=5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertEquals(5, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInteger() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:int=5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||||
|
Assert.assertEquals(5, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLong() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:long=5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Long);
|
||||||
|
Assert.assertEquals(5L, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFloat() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:float=4.5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Float);
|
||||||
|
Assert.assertEquals(4.5f, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testString() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=some_value");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof String);
|
||||||
|
Assert.assertEquals("some_value", option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStringEscaped() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof String);
|
||||||
|
Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testList() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c");
|
||||||
|
|
||||||
|
Assert.assertEquals(5, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option;
|
||||||
|
|
||||||
|
option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("a", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||||
|
Assert.assertEquals(1, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(1);
|
||||||
|
Assert.assertEquals("b", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||||
|
Assert.assertEquals(2, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(2);
|
||||||
|
Assert.assertEquals("c", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Long);
|
||||||
|
Assert.assertEquals(3L, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(3);
|
||||||
|
Assert.assertEquals("d", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Float);
|
||||||
|
Assert.assertEquals(4.5f, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(4);
|
||||||
|
Assert.assertEquals("e", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof String);
|
||||||
|
Assert.assertEquals("a,b=c", option.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -229,6 +229,30 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals("testé", event.getText());
|
Assert.assertEquals("testé", event.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseBigSetClipboardEvent() throws IOException {
|
||||||
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
|
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||||
|
|
||||||
|
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
|
||||||
|
Arrays.fill(rawText, (byte) 'a');
|
||||||
|
String text = new String(rawText, 0, rawText.length);
|
||||||
|
|
||||||
|
dos.writeShort(rawText.length);
|
||||||
|
dos.write(rawText);
|
||||||
|
|
||||||
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
|
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||||
|
Assert.assertEquals(text, event.getText());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseSetScreenPowerMode() throws IOException {
|
public void testParseSetScreenPowerMode() throws IOException {
|
||||||
ControlMessageReader reader = new ControlMessageReader();
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|||||||
Reference in New Issue
Block a user