From 7a77f3bab47931c67664ba43b490f15eab617d0a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 May 2020 10:11:05 +0200 Subject: [PATCH] Copy on "get clipboard" if possible Ctrl+c synchronizes the Android device clipboard to the computer clipboard. To make the copy more straightforward, if the device runs at least Android 7, also send a COPY keycode before copying the clipboard. --- app/src/control_msg.c | 4 +++- app/src/control_msg.h | 3 +++ app/src/input_manager.c | 6 ++++-- app/tests/test_control_msg_serialize.c | 6 +++++- .../com/genymobile/scrcpy/ControlMessage.java | 10 ++++++++++ .../scrcpy/ControlMessageReader.java | 13 ++++++++++++- .../com/genymobile/scrcpy/Controller.java | 19 +++++++++++++++---- .../java/com/genymobile/scrcpy/Device.java | 8 ++++++++ .../scrcpy/ControlMessageReaderTest.java | 2 ++ 9 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index c5778c02..4bd1c74d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -66,6 +66,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[17], (uint32_t) msg->inject_scroll_event.vscroll); return 21; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + buf[1] = msg->get_clipboard.copy; + return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { buf[1] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, @@ -79,7 +82,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: - case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 0e85c97e..6aa14a3c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -60,6 +60,9 @@ struct control_msg { int32_t hscroll; int32_t vscroll; } inject_scroll_event; + struct { + bool copy; + } get_clipboard; struct { char *text; // owned, to be freed by SDL_free() bool paste; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e8ba9f79..d4840682 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -102,9 +102,10 @@ collapse_notification_panel(struct controller *controller) { } static void -request_device_clipboard(struct controller *controller) { +request_device_clipboard(struct controller *controller, bool copy) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; + msg.get_clipboard.copy = copy; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request device clipboard"); @@ -352,7 +353,8 @@ input_manager_process_key(struct input_manager *im, return; case SDLK_c: if (control && cmd && !shift && !repeat && down) { - request_device_clipboard(controller); + // press COPY and get the clipboard content + request_device_clipboard(controller, true); } return; case SDLK_v: diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c6ff7b2e..3ffd2e6c 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -185,14 +185,18 @@ static void test_serialize_collapse_notification_panel(void) { static void test_serialize_get_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + .get_clipboard = { + .copy = true, + }, }; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 1); + assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_GET_CLIPBOARD, + 1, // copy }; assert(!memcmp(buf, expected, sizeof(expected))); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 7d0ab7a6..32b2172e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -18,6 +18,7 @@ public final class ControlMessage { public static final int TYPE_ROTATE_DEVICE = 10; public static final int FLAGS_PASTE = 1; + public static final int FLAGS_COPY = 2; private int type; private String text; @@ -81,6 +82,15 @@ public final class ControlMessage { return msg; } + public static ControlMessage createGetClipboard(boolean copy) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_GET_CLIPBOARD; + if (copy) { + msg.flags = FLAGS_COPY; + } + return msg; + } + /** * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants */ diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index fbf49a61..ec824b5c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -12,6 +12,7 @@ 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 GET_CLIPBOARD_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length) @@ -67,6 +68,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = parseGetClipboard(); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; @@ -76,7 +80,6 @@ public class ControlMessageReader { case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: - case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; @@ -148,6 +151,14 @@ public class ControlMessageReader { return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); } + private ControlMessage parseGetClipboard() { + if (buffer.remaining() < GET_CLIPBOARD_PAYLOAD_LENGTH) { + return null; + } + boolean copy = buffer.get() != 0; + return ControlMessage.createGetClipboard(copy); + } + private ControlMessage parseSetClipboard() { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index d264ece9..b989bc11 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -104,10 +104,8 @@ public class Controller { device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = device.getClipboardText(); - if (clipboardText != null) { - sender.pushClipboardText(clipboardText); - } + boolean copy = (msg.getFlags() & ControlMessage.FLAGS_COPY) != 0; + getClipboard(copy); break; case ControlMessage.TYPE_SET_CLIPBOARD: boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; @@ -229,6 +227,19 @@ public class Controller { return device.injectKeycode(keycode); } + private void getClipboard(boolean copy) { + // On Android >= 7, also press the COPY key if requested + if (copy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + // Must wait until the COPY has been executed + device.injectCopyKeycode(); + } + + String clipboardText = device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } + } + private boolean setClipboard(String text, boolean paste) { boolean ok = device.setClipboardText(text); if (ok) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 993af001..b0ec9f9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -186,6 +186,14 @@ public final class Device { return injectKeycode(keyCode, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } + public boolean injectCopyKeycode() { + isSettingClipboard.set(true); + // Must wait until the COPY has been executed + boolean ret = injectKeycode(KeyEvent.KEYCODE_COPY, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); + isSettingClipboard.set(false); + return ret; + } + public boolean injectPasteKeycode() { return injectKeycode(KeyEvent.KEYCODE_PASTE); } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f5fa4d09..f4f21549 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -200,6 +200,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + dos.writeByte(1); // copy byte[] packet = bos.toByteArray(); @@ -207,6 +208,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + Assert.assertEquals(1, (event.getFlags() & ControlMessage.FLAGS_COPY)); } @Test