Compare commits

...
9 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
11 changed files with 141 additions and 51 deletions
+16
View File
@@ -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:
``` ```
+5 -1
View File
@@ -52,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.
@@ -146,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).
+10
View File
@@ -54,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"
@@ -516,6 +520,7 @@ guess_record_format(const char *filename) {
#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_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[]) {
@@ -525,6 +530,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, {"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,
@@ -701,6 +708,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_CODEC_OPTIONS: case OPT_CODEC_OPTIONS:
opts->codec_options = optarg; opts->codec_options = optarg;
break; 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;
+1
View File
@@ -305,6 +305,7 @@ scrcpy(const struct scrcpy_options *options) {
.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, .codec_options = options->codec_options,
.force_adb_forward = options->force_adb_forward,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
return false; return false;
+2
View File
@@ -42,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 { \
@@ -79,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
+14 -8
View File
@@ -217,15 +217,20 @@ 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);
} }
@@ -384,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;
} }
+1
View File
@@ -56,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
@@ -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);
} }
} }
@@ -10,8 +10,14 @@ 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.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 {
@@ -31,6 +37,7 @@ public final class Device {
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private ClipboardListener clipboardListener; private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/** /**
* Logical display identifier * Logical display identifier
@@ -76,6 +83,10 @@ public final class Device {
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override @Override
public void dispatchPrimaryClipChanged() { public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it
return;
}
synchronized (Device.this) { synchronized (Device.this) {
if (clipboardListener != null) { if (clipboardListener != null) {
String text = getClipboardText(); String text = getClipboardText();
@@ -140,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()");
} }
@@ -152,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();
} }
@@ -180,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,7 +15,7 @@ public final class Ln {
DEBUG, INFO, WARN, ERROR DEBUG, INFO, WARN, ERROR
} }
private static Level THRESHOLD; private static Level threshold;
private Ln() { private Ln() {
// not instantiable // not instantiable
@@ -29,11 +29,11 @@ public final class Ln {
* @param level the log level * @param level the log level
*/ */
public static void initLogLevel(Level level) { public static void initLogLevel(Level level) {
THRESHOLD = 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) {
@@ -9,6 +9,7 @@ import android.os.Build;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Locale;
public final class Server { public final class Server {
@@ -126,7 +127,7 @@ public final class Server {
Options options = new Options(); Options options = new Options();
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase()); Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
options.setLogLevel(level); options.setLogLevel(level);
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8 int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8