From fc1dec027093ee8a2746a212db0c72a9d96d39a4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 May 2020 20:58:24 +0200 Subject: [PATCH] Paste on "set clipboard" if possible Ctrl+Shift+v synchronizes the computer clipboard to the Android device clipboard. This feature had been added to provide a way to copy UTF-8 text from the computer to the device. To make such a paste more straightforward, if the device runs at least Android 7, also send a PASTE keycode to paste immediately. Fixes #786 --- README.md | 49 ++++++++++--------- app/scrcpy.1 | 2 +- app/src/cli.c | 3 +- app/src/control_msg.c | 5 +- app/src/control_msg.h | 5 +- app/src/input_manager.c | 7 +-- app/tests/test_control_msg_serialize.c | 4 +- .../com/genymobile/scrcpy/ControlMessage.java | 12 ++++- .../scrcpy/ControlMessageReader.java | 9 +++- .../com/genymobile/scrcpy/Controller.java | 21 ++++++-- .../scrcpy/ControlMessageReaderTest.java | 8 +++ 11 files changed, 84 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3f4db9fa..7cc7e1fd 100644 --- a/README.md +++ b/README.md @@ -494,7 +494,8 @@ It is possible to synchronize clipboards between the computer and the device, in both directions: - `Ctrl`+`c` copies the device clipboard to the computer clipboard; - - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; + - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard (and + pastes if the device runs Android >= 7); - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but breaks non-ASCII characters). @@ -559,29 +560,29 @@ Also see [issue #14]. ## Shortcuts - | Action | Shortcut | Shortcut (macOS) - | -------------------------------------- |:----------------------------- |:----------------------------- - | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` - | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ - | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ - | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` - | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ - | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ - | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` - | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` - | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ - | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` - | Power on | _Right-click²_ | _Right-click²_ - | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` - | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` - | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` - | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` - | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` - | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` - | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` + | Action | Shortcut | Shortcut (macOS) + | ------------------------------------------- |:----------------------------- |:----------------------------- + | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ + | Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ + | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` + | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ + | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ + | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ + | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` + | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` + | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ + | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ + | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` + | Power on | _Right-click²_ | _Right-click²_ + | Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o` + | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` + | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` + | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` + | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` + | Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1020a2fb..ed4594ed 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -280,7 +280,7 @@ Paste computer clipboard to device .TP .B Ctrl+Shift+v -Copy computer clipboard to device +Copy computer clipboard to device (and paste if the device runs Android >= 7) .TP .B Ctrl+i diff --git a/app/src/cli.c b/app/src/cli.c index a0c17c1a..836edc30 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -247,7 +247,8 @@ scrcpy_print_usage(const char *arg0) { " Paste computer clipboard to device\n" "\n" " " CTRL_OR_CMD "+Shift+v\n" - " Copy computer clipboard to device\n" + " Copy computer clipboard to device (and paste if the device\n" + " runs Android >= 7)\n" "\n" " " CTRL_OR_CMD "+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 72504138..c5778c02 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -67,10 +67,11 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { (uint32_t) msg->inject_scroll_event.vscroll); return 21; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { + buf[1] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[1]); - return 1 + len; + &buf[2]); + return 2 + len; } case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index e132fc6b..0e85c97e 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -11,9 +11,9 @@ #include "common.h" #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4092 #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ - (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) + (4 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) #define POINTER_ID_MOUSE UINT64_C(-1); @@ -62,6 +62,7 @@ struct control_msg { } inject_scroll_event; struct { char *text; // owned, to be freed by SDL_free() + bool paste; } set_clipboard; struct { enum screen_power_mode mode; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e8c8d68e..3cb9e3ac 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -112,7 +112,7 @@ request_device_clipboard(struct controller *controller) { } static void -set_device_clipboard(struct controller *controller) { +set_device_clipboard(struct controller *controller, bool paste) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -127,6 +127,7 @@ set_device_clipboard(struct controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.set_clipboard.text = text; + msg.set_clipboard.paste = paste; if (!controller_push_msg(controller, &msg)) { SDL_free(text); @@ -354,8 +355,8 @@ input_manager_process_key(struct input_manager *im, case SDLK_v: if (control && cmd && !repeat && down) { if (shift) { - // store the text in the device clipboard - set_device_clipboard(controller); + // store the text in the device clipboard and paste + set_device_clipboard(controller, true); } else { // inject the text as input events clipboard_paste(controller); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index da243d91..c6ff7b2e 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -201,16 +201,18 @@ static void test_serialize_set_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { + .paste = true, .text = "hello, world!", }, }; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 17); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_CLIPBOARD, + 1, // paste 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 195b04bf..7d0ab7a6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -17,6 +17,8 @@ public final class ControlMessage { public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_ROTATE_DEVICE = 10; + public static final int FLAGS_PASTE = 1; + private int type; private String text; private int metaState; // KeyEvent.META_* @@ -28,6 +30,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; + private int flags; private ControlMessage() { } @@ -68,10 +71,13 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text) { + public static ControlMessage createSetClipboard(String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; + if (paste) { + msg.flags = FLAGS_PASTE; + } return msg; } @@ -134,4 +140,8 @@ public final class ControlMessage { public int getVScroll() { return vScroll; } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2688641c..fbf49a61 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -12,8 +12,9 @@ public class ControlMessageReader { static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) public static final int INJECT_TEXT_MAX_LENGTH = 300; private static final int RAW_BUFFER_SIZE = 4096; @@ -148,11 +149,15 @@ public class ControlMessageReader { } private ControlMessage parseSetClipboard() { + if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { + return null; + } + boolean parse = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text); + return ControlMessage.createSetClipboard(text, parse); } private ControlMessage parseSetScreenPowerMode() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index ab7b6c40..4999f7eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; import android.view.KeyCharacterMap; @@ -107,10 +108,8 @@ public class Controller { sender.pushClipboardText(clipboardText); break; case ControlMessage.TYPE_SET_CLIPBOARD: - boolean setClipboardOk = device.setClipboardText(msg.getText()); - if (setClipboardOk) { - Ln.i("Device clipboard set"); - } + boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + setClipboard(msg.getText(), paste); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { @@ -227,4 +226,18 @@ public class Controller { int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; return device.injectKeycode(keycode); } + + private boolean setClipboard(String text, boolean paste) { + boolean ok = device.setClipboardText(text); + if (ok) { + Ln.i("Device clipboard set"); + } + + // On Android >= 7, also press the PASTE key if requested + if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + device.injectKeycode(KeyEvent.KEYCODE_PASTE); + } + + return ok; + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index d495b44c..f5fa4d09 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -216,6 +216,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeShort(text.length); dos.write(text); @@ -227,6 +228,9 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); + + boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + Assert.assertTrue(parse); } @Test @@ -238,6 +242,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeByte(1); // paste Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); @@ -251,6 +256,9 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(text, event.getText()); + + boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0; + Assert.assertTrue(parse); } @Test