Compare commits

...

16 Commits

Author SHA1 Message Date
Romain Vimont
d613b10efc Add a new method for text injection
On Android >= 7, inject text using the clipboard.

This is faster and allows to inject UTF-8.
2020-05-25 03:28:33 +02:00
Romain Vimont
cc4e1e20de Add more convenience methods for injection
Expose methods to inject key events and key codes with an additional
parameter to specify the "mode" (async, wait for finish, wait for
result).
2020-05-25 03:28:33 +02:00
Romain Vimont
4bbabfb4ef Move injection methods to Device
Only the main injection method was exposed on Device, the convenience
methods were implemented in Controller.

For consistency, move them all to the Device class.
2020-05-25 03:28:33 +02:00
Romain Vimont
ffc57512b3 Avoid clipboard synchronization loop
The Android device listens for clipboard changes to synchronize with the
computer clipboard.

However, if the change comes from scrcpy (for example via Ctrl+Shift+v),
do not notify the change.
2020-05-25 03:28:33 +02:00
Romain Vimont
c7a33fac36 Log actions on the caller side
Some actions are exposed by the Device class, but logging success should
be done by the caller.
2020-05-25 03:28:33 +02:00
Romain Vimont
81573d81a0 Pass a Locale to toUpperCase()
Make lint happy.
2020-05-25 03:28:15 +02:00
Romain Vimont
5c2cf88a1d Rename THRESHOLD to threshold
Since the field is not final anymore, lint expects the name not to be
capitalized.
2020-05-25 03:22:07 +02:00
Romain Vimont
8f46e18426 Add --force-adb-forward
Add a command-line option to force "adb forward", without attempting
"adb reverse" first.

This is especially useful for using SSH tunnels without enabling remote
port forwarding.
2020-05-24 23:28:31 +02:00
Romain Vimont
ee3882f8be Fix typo in manpage 2020-05-24 23:14:30 +02:00
Romain Vimont
a3ef461d73 Add cli option to set the verbosity level
The verbosity was set either to info (in release mode) or debug (in
debug mode).

Add a command-line argument to change it, so that users can enable debug
logs using the release:

    scrcpy -Vdebug
2020-05-24 22:01:51 +02:00
Romain Vimont
3df63c579d Configure server verbosity from the client
Send the requested log level from the client.

This paves the way to configure it via a command-line argument.
2020-05-24 21:14:25 +02:00
Romain Vimont
56bff2f718 Avoid compiler warning
The field lock_video_orientation may only take values between -1 and 3
(included). But the compiler may trigger a warning on the sprintf()
call, because its type could represent values which could overflow the
string (like "-128"):

> warning: ‘%i’ directive writing between 1 and 4 bytes into a region of
> size 3 [-Wformat-overflow=]

Increase the buffer size to remove the warning.
2020-05-24 21:11:21 +02:00
Tzah Mazuz
080a4ee365 Add --codec-options
Add a command-line parameter to pass custom options to the device
encoder (as a comma-separated list of "key[:type]=value").

The list of possible codec options is available in the Android
documentation:
<https://d.android.com/reference/android/media/MediaFormat>

PR #1325 <https://github.com/Genymobile/scrcpy/pull/1325>
Refs #1226 <https://github.com/Genymobile/scrcpy/pull/1226>

Co-authored-by: Romain Vimont <rom@rom1v.com>
2020-05-24 14:54:34 +02:00
Romain Vimont
c7155a1954 Add unit test for big "set clipboard" event
Add a unit test to avoid regressions.

Refs #1425 <https://github.com/Genymobile/scrcpy/issues/1425>
2020-05-24 14:33:43 +02:00
Romain Vimont
517dbd9c85 Increase buffer size to fix "set clipboard" event
The buffer size must be greater than any event message.

Clipboard events may take up to 4096 bytes, so increase the buffer size.

Fixes #1425 <https://github.com/Genymobile/scrcpy/issues/1425>
2020-05-24 14:33:23 +02:00
Romain Vimont
acc4ef31df Synchronize device clipboard to computer
Automatically synchronize the device clipboard to the computer any time
it changes.

This allows seamless copy-paste from Android to the computer.

Fixes #1056 <https://github.com/Genymobile/scrcpy/issues/1056#issuecomment-631363684>
PR #1423 <https://github.com/Genymobile/scrcpy/pull/1423>
2020-05-24 14:31:49 +02:00
21 changed files with 706 additions and 75 deletions

View File

@@ -289,6 +289,22 @@ From another terminal:
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:
```
@@ -482,6 +498,9 @@ both directions:
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
breaks non-ASCII characters).
Moreover, any time the Android clipboard changes, it is automatically
synchronized to the computer clipboard.
#### Text injection preference
There are two kinds of [events][textevents] generated when typing text:

View File

@@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
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
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
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.
.TP
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
@@ -136,7 +150,7 @@ Turn the device screen off immediately.
.TP
.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).
@@ -144,6 +158,12 @@ It only shows physical touches (not clicks from scrcpy).
.B \-v, \-\-version
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
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running.

View File

@@ -30,6 +30,15 @@ scrcpy_print_usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\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 the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
@@ -45,6 +54,10 @@ scrcpy_print_usage(const char *arg0) {
"\n"
" Default is 0.\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"
" Start in fullscreen.\n"
"\n"
@@ -136,6 +149,14 @@ scrcpy_print_usage(const char *arg0) {
"\n"
" -v, --version\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"
" -w, --stay-awake\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;
}
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
parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) {
@@ -472,14 +519,19 @@ guess_record_format(const char *filename) {
#define OPT_ROTATION 1015
#define OPT_RENDER_DRIVER 1016
#define OPT_NO_MIPMAPS 1017
#define OPT_CODEC_OPTIONS 1018
#define OPT_FORCE_ADB_FORWARD 1019
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'},
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
{"crop", required_argument, NULL, OPT_CROP},
{"display", required_argument, NULL, OPT_DISPLAY_ID},
{"force-adb-forward", no_argument, NULL,
OPT_FORCE_ADB_FORWARD},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"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'},
{"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"verbosity", required_argument, NULL, 'V'},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"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
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options,
NULL)) != -1) {
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
long_options, NULL)) != -1) {
switch (c) {
case 'b':
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':
args->version = true;
break;
case 'V':
if (!parse_log_level(optarg, &opts->log_level)) {
return false;
}
break;
case 'w':
opts->stay_awake = true;
break;
@@ -647,6 +705,12 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_NO_MIPMAPS:
opts->mipmaps = false;
break;
case OPT_CODEC_OPTIONS:
opts->codec_options = optarg;
break;
case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true;
break;
default:
// getopt prints the error message on stderr
return false;

View File

@@ -29,6 +29,24 @@ print_version(void) {
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
main(int argc, char *argv[]) {
#ifdef __WINDOWS__
@@ -38,20 +56,23 @@ main(int argc, char *argv[]) {
setbuf(stderr, NULL);
#endif
#ifndef NDEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = false,
};
#ifndef NDEBUG
args.opts.log_level = SC_LOG_LEVEL_DEBUG;
#endif
if (!scrcpy_parse_args(&args, argc, argv)) {
return 1;
}
SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level);
SDL_LogSetAllPriority(sdl_log);
if (args.help) {
scrcpy_print_usage(argv[0]);
return 0;

View File

@@ -293,6 +293,7 @@ bool
scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
struct server_params params = {
.log_level = options->log_level,
.crop = options->crop,
.port_range = options->port_range,
.max_size = options->max_size,
@@ -303,6 +304,8 @@ scrcpy(const struct scrcpy_options *options) {
.display_id = options->display_id,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.codec_options = options->codec_options,
.force_adb_forward = options->force_adb_forward,
};
if (!server_start(&server, options->serial, &params)) {
return false;

View File

@@ -8,6 +8,7 @@
#include "common.h"
#include "input_manager.h"
#include "recorder.h"
#include "util/log.h"
struct scrcpy_options {
const char *serial;
@@ -16,6 +17,8 @@ struct scrcpy_options {
const char *window_title;
const char *push_target;
const char *render_driver;
const char *codec_options;
enum sc_log_level log_level;
enum recorder_format record_format;
struct port_range port_range;
uint16_t max_size;
@@ -39,6 +42,7 @@ struct scrcpy_options {
bool window_borderless;
bool mipmaps;
bool stay_awake;
bool force_adb_forward;
};
#define SCRCPY_OPTIONS_DEFAULT { \
@@ -48,6 +52,8 @@ struct scrcpy_options {
.window_title = NULL, \
.push_target = NULL, \
.render_driver = NULL, \
.codec_options = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \
.record_format = RECORDER_FORMAT_AUTO, \
.port_range = { \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
@@ -74,6 +80,7 @@ struct scrcpy_options {
.window_borderless = false, \
.mipmaps = true, \
.stay_awake = false, \
.force_adb_forward = false, \
}
bool

View File

@@ -217,24 +217,46 @@ enable_tunnel_forward_any_port(struct server *server,
}
static bool
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
if (enable_tunnel_reverse_any_port(server, port_range)) {
return true;
enable_tunnel_any_port(struct server *server, struct port_range port_range,
bool force_adb_forward) {
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);
}
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
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 lock_video_orientation_string[5];
char display_id_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
@@ -259,6 +281,7 @@ execute_server(struct server *server, const struct server_params *params) {
"/", // unused
"com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
log_level_to_server_string(params->log_level),
max_size_string,
bit_rate_string,
max_fps_string,
@@ -270,6 +293,7 @@ execute_server(struct server *server, const struct server_params *params) {
display_id_string,
params->show_touches ? "true" : "false",
params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-",
};
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
@@ -365,7 +389,8 @@ server_start(struct server *server, const char *serial,
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;
}

View File

@@ -9,6 +9,7 @@
#include "config.h"
#include "command.h"
#include "common.h"
#include "util/log.h"
#include "util/net.h"
struct server {
@@ -43,7 +44,9 @@ struct server {
}
struct server_params {
enum sc_log_level log_level;
const char *crop;
const char *codec_options;
struct port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
@@ -53,6 +56,7 @@ struct server_params {
uint16_t display_id;
bool show_touches;
bool stay_awake;
bool force_adb_forward;
};
// init default values

View File

@@ -3,6 +3,13 @@
#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 LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)

View File

@@ -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();
}

View 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);
}
}

View File

@@ -15,7 +15,8 @@ public class ControlMessageReader {
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
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 ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);

View File

@@ -1,10 +1,7 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -50,7 +47,7 @@ public class Controller {
public void control() throws IOException {
// on start, power on the device
if (!device.isScreenOn()) {
injectKeycode(KeyEvent.KEYCODE_POWER);
device.injectKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
@@ -110,11 +107,18 @@ public class Controller {
sender.pushClipboardText(clipboardText);
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText());
boolean setClipboardOk = device.setClipboardText(msg.getText());
if (setClipboardOk) {
Ln.i("Device clipboard set");
}
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
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;
case ControlMessage.TYPE_ROTATE_DEVICE:
@@ -126,7 +130,7 @@ public class Controller {
}
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) {
@@ -137,7 +141,7 @@ public class Controller {
return false;
}
for (KeyEvent event : events) {
if (!injectEvent(event)) {
if (!device.injectEvent(event)) {
return false;
}
}
@@ -145,6 +149,10 @@ public class Controller {
}
private int injectText(String text) {
if (device.injectTextPaste(text)) {
// The best method (fastest and UTF-8) worked!
return text.length();
}
int successCount = 0;
for (char c : text.toCharArray()) {
if (!injectChar(c)) {
@@ -193,7 +201,7 @@ public class Controller {
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event);
return device.injectEvent(event);
}
private boolean injectScroll(Position position, int hScroll, int vScroll) {
@@ -216,26 +224,11 @@ public class Controller {
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return 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);
return device.injectEvent(event);
}
private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode);
return device.injectKeycode(keycode);
}
}

View File

@@ -6,12 +6,18 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
public final class Device {
@@ -22,10 +28,16 @@ public final class Device {
void onRotationChanged(int rotation);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
private final ServiceManager serviceManager = new ServiceManager();
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/**
* Logical display identifier
@@ -66,6 +78,27 @@ public final class Device {
}
}, 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) {
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;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
public boolean injectEvent(InputEvent inputEvent, int mode) {
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);
}
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() {
return serviceManager.getPowerManager().isScreenOn();
}
@@ -138,6 +215,10 @@ public final class Device {
this.rotationListener = rotationListener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}
public void expandNotificationPanel() {
serviceManager.getStatusBarManager().expandNotificationsPanel();
}
@@ -154,26 +235,27 @@ public final class Device {
return s.toString();
}
public void setClipboardText(String text) {
boolean ok = serviceManager.getClipboardManager().setText(text);
if (ok) {
Ln.i("Device clipboard set");
}
private boolean rawSetClipboardText(String text) {
return serviceManager.getClipboardManager().setText(text);
}
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
*/
public void setScreenPowerMode(int mode) {
public boolean setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");
return;
}
boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
if (ok) {
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
return false;
}
return SurfaceControl.setDisplayPowerMode(d, mode);
}
/**

View File

@@ -15,14 +15,25 @@ public final class Ln {
DEBUG, INFO, WARN, ERROR
}
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
private static Level threshold;
private Ln() {
// 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) {
return level.ordinal() >= THRESHOLD.ordinal();
return level.ordinal() >= threshold.ordinal();
}
public static void d(String message) {

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
public class Options {
private Ln.Level logLevel;
private int maxSize;
private int bitRate;
private int maxFps;
@@ -14,6 +15,15 @@ public class Options {
private int displayId;
private boolean showTouches;
private boolean stayAwake;
private String codecOptions;
public Ln.Level getLogLevel() {
return logLevel;
}
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getMaxSize() {
return maxSize;
@@ -102,4 +112,12 @@ public class Options {
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public String getCodecOptions() {
return codecOptions;
}
public void setCodecOptions(String codecOptions) {
this.codecOptions = codecOptions;
}
}

View File

@@ -12,6 +12,7 @@ import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener {
@@ -25,15 +26,17 @@ public class ScreenEncoder implements Device.RotationListener {
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private List<CodecOption> codecOptions;
private int bitRate;
private int maxFps;
private boolean sendFrameMeta;
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.bitRate = bitRate;
this.maxFps = maxFps;
this.codecOptions = codecOptions;
}
@Override
@@ -61,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener {
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this);
boolean alive;
try {
@@ -151,7 +154,24 @@ public class ScreenEncoder implements Device.RotationListener {
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();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
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>
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
}
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
setCodecOption(format, option);
}
}
return format;
}

View File

@@ -8,6 +8,8 @@ import android.os.BatteryManager;
import android.os.Build;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
public final class Server {
@@ -19,6 +21,7 @@ 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);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1;
@@ -49,15 +52,23 @@ public final class Server {
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());
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
if (options.getControl()) {
Controller controller = new Controller(device, connection);
final Controller controller = new Controller(device, connection);
// asynchronous
startController(controller);
startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
}
try {
@@ -109,47 +120,53 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
final int expectedParameters = 12;
final int expectedParameters = 14;
if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
}
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);
int bitRate = Integer.parseInt(args[2]);
int bitRate = Integer.parseInt(args[3]);
options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]);
int maxFps = Integer.parseInt(args[4]);
options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[4]);
int lockedVideoOrientation = Integer.parseInt(args[5]);
options.setLockedVideoOrientation(lockedVideoOrientation);
// 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);
Rect crop = parseCrop(args[6]);
Rect crop = parseCrop(args[7]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[8]);
boolean control = Boolean.parseBoolean(args[9]);
options.setControl(control);
int displayId = Integer.parseInt(args[9]);
int displayId = Integer.parseInt(args[10]);
options.setDisplayId(displayId);
boolean showTouches = Boolean.parseBoolean(args[10]);
boolean showTouches = Boolean.parseBoolean(args[11]);
options.setShowTouches(showTouches);
boolean stayAwake = Boolean.parseBoolean(args[11]);
boolean stayAwake = Boolean.parseBoolean(args[12]);
options.setStayAwake(stayAwake);
String codecOptions = args[13];
options.setCodecOptions(codecOptions);
return options;
}
@@ -202,6 +219,9 @@ public final class Server {
});
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
scrcpy(options);
}
}

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;
@@ -13,6 +14,7 @@ public class ClipboardManager {
private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
public ClipboardManager(IInterface manager) {
this.manager = manager;
@@ -81,4 +83,37 @@ public class ClipboardManager {
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;
}
}
}

View 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());
}
}

View File

@@ -229,6 +229,30 @@ public class ControlMessageReaderTest {
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
public void testParseSetScreenPowerMode() throws IOException {
ControlMessageReader reader = new ControlMessageReader();