diff --git a/README.md b/README.md index 2f1d4f5d..30a90a34 100644 --- a/README.md +++ b/README.md @@ -582,7 +582,7 @@ _`Meta` is typically the `Windows` key on the keyboard, or `Cmd` on macOS._ | Rotate device screen | `Meta`+`r` | Expand notification panel | `Meta`+`n` | Collapse notification panel | `Meta`+`Shift`+`n` - | Copy device clipboard to computer | `Meta`+`c` + | Press COPY³, then Copy device clipboard to computer | `Meta`+`c` | Paste computer clipboard to device | `Meta`+`v` | Copy computer clipboard to device, then press PASTE³ | `Meta`+`Shift`+`v` | Enable/disable FPS counter (on stdout) | `Meta`+`i` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a63a44aa..dcef302f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -277,7 +277,7 @@ Collapse notification panel .TP .B Meta+c -Copy device clipboard to computer +Press COPY (Android >= 7), then copy device clipboard to computer .TP .B Meta+v diff --git a/app/src/cli.c b/app/src/cli.c index a13d8a44..c2ffdef1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -245,7 +245,8 @@ scrcpy_print_usage(const char *arg0) { " Collapse notification panel\n" "\n" " " MOD "+c\n" - " Copy device clipboard to computer\n" + " Press COPY (Android >= 7), then copy device clipboard to\n" + " computer\n" "\n" " " MOD "+v\n" " Paste computer clipboard to device\n" 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 59734c64..281d4086 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"); @@ -341,7 +342,8 @@ input_manager_process_key(struct input_manager *im, return; case SDLK_c: if (control && !shift && !repeat && down) { - request_device_clipboard(controller); + // Press COPY, then 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 8415b046..f07ffef5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -28,7 +28,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; - private boolean paste; + private boolean pressCopyOrPaste; private ControlMessage() { } @@ -69,11 +69,18 @@ public final class ControlMessage { return msg; } + public static ControlMessage createGetClipboard(boolean copy) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_GET_CLIPBOARD; + msg.pressCopyOrPaste = copy; + return msg; + } + public static ControlMessage createSetClipboard(String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; - msg.paste = paste; + msg.pressCopyOrPaste = paste; return msg; } @@ -137,7 +144,7 @@ public final class ControlMessage { return vScroll; } - public boolean getPaste() { - return paste; + public boolean getPressCopyOrPaste() { + return pressCopyOrPaste; } } 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 38472441..9abd782a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -104,13 +104,10 @@ public class Controller { device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = device.getClipboardText(); - if (clipboardText != null) { - sender.pushClipboardText(clipboardText); - } + getClipboard(msg.getPressCopyOrPaste()); break; case ControlMessage.TYPE_SET_CLIPBOARD: - setClipboard(msg.getText(), msg.getPaste()); + setClipboard(msg.getText(), msg.getPressCopyOrPaste()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { @@ -228,6 +225,23 @@ public class Controller { return device.injectKeycode(keycode); } + private boolean 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()) { + // If there is something to copy, the clipboard will be automatically sent to the computer clipboard via the ClipboardListener + return device.injectKeycode(KeyEvent.KEYCODE_COPY); + } + + // We can't press COPY, so only synchronize the current clipboard + String clipboardText = device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + return true; + } + + return false; + } + private boolean setClipboard(String text, boolean paste) { boolean ok = device.setClipboardText(text); if (ok) { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index db92deec..fe3f4c3f 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.assertTrue(event.getPressCopyOrPaste()); } @Test @@ -228,7 +230,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); - Assert.assertTrue(event.getPaste()); + Assert.assertTrue(event.getPressCopyOrPaste()); } @Test @@ -254,7 +256,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(text, event.getText()); - Assert.assertTrue(event.getPaste()); + Assert.assertTrue(event.getPressCopyOrPaste()); } @Test