From e2b3968c66ff1310d7644aef92dcb543604e1b6b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 09:30:57 +0100 Subject: [PATCH] Always synchronize clipboard on explicit COPY/CUT If --no-clipboard-autosync is enabled, the automatic clipboard synchronization performed whenever the device clipboard changes is disabled. But on explicit COPY and CUT scrcpy shortcuts (MOD+c and MOD+x), the clipboard should still be synchronized, so that it remains possible to copy-paste from the device to the computer. This is consistent with the behavior of MOD+v, which pastes the computer clipboard to the device. Refs #2228 Refs #2817 PR #2834 --- app/src/control_msg.c | 17 ++++++--- app/src/control_msg.h | 9 +++++ app/src/input_manager.c | 35 +++++++++++-------- app/tests/test_control_msg_serialize.c | 6 +++- .../com/genymobile/scrcpy/ControlMessage.java | 16 +++++++++ .../scrcpy/ControlMessageReader.java | 13 ++++++- .../com/genymobile/scrcpy/Controller.java | 28 ++++++++++++--- .../java/com/genymobile/scrcpy/Server.java | 2 +- .../scrcpy/ControlMessageReaderTest.java | 2 ++ 9 files changed, 102 insertions(+), 26 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 4cc2f9d7..7fd77631 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -55,6 +55,12 @@ static const char *const screen_power_mode_labels[] = { "suspend", }; +static const char *const copy_key_labels[] = { + "none", + "copy", + "cut", +}; + static void write_position(uint8_t *buf, const struct sc_position *position) { buffer_write32be(&buf[0], position->point.x); @@ -117,6 +123,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + buf[1] = msg->get_clipboard.copy_key; + return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { buffer_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; @@ -131,7 +140,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_PANELS: - case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; @@ -194,6 +202,10 @@ control_msg_log(const struct control_msg *msg) { LOG_CMSG("back-or-screen-on %s", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); break; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + LOG_CMSG("get clipboard copy_key=%s", + copy_key_labels[msg->get_clipboard.copy_key]); + break; case CONTROL_MSG_TYPE_SET_CLIPBOARD: LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", msg->set_clipboard.sequence, @@ -213,9 +225,6 @@ control_msg_log(const struct control_msg *msg) { case CONTROL_MSG_TYPE_COLLAPSE_PANELS: LOG_CMSG("collapse panels"); break; - case CONTROL_MSG_TYPE_GET_CLIPBOARD: - LOG_CMSG("get clipboard"); - break; case CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 7352defe..6f1824bb 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,12 @@ enum screen_power_mode { SCREEN_POWER_MODE_NORMAL = 2, }; +enum get_clipboard_copy_key { + GET_CLIPBOARD_COPY_KEY_NONE, + GET_CLIPBOARD_COPY_KEY_COPY, + GET_CLIPBOARD_COPY_KEY_CUT, +}; + struct control_msg { enum control_msg_type type; union { @@ -69,6 +75,9 @@ struct control_msg { enum android_keyevent_action action; // action for the BACK key // screen may only be turned on on ACTION_DOWN } back_or_screen_on; + struct { + enum get_clipboard_copy_key copy_key; + } get_clipboard; struct { uint64_t sequence; char *text; // owned, to be freed by free() diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 31d6540f..9a553842 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -148,16 +148,6 @@ action_menu(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); } -static inline void -action_copy(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_COPY, actions, "COPY"); -} - -static inline void -action_cut(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_CUT, actions, "CUT"); -} - // turn the screen on if it was off, press BACK otherwise // If the screen is off, it is turned on only on ACTION_DOWN static void @@ -211,6 +201,21 @@ collapse_panels(struct controller *controller) { } } +static bool +get_device_clipboard(struct controller *controller, + enum get_clipboard_copy_key copy_key) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; + msg.get_clipboard.copy_key = copy_key; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'get device clipboard'"); + return false; + } + + return true; +} + static bool set_device_clipboard(struct controller *controller, bool paste, uint64_t sequence) { @@ -450,13 +455,15 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_c: - if (control && !shift && !repeat) { - action_copy(controller, action); + if (control && !shift && !repeat && down) { + get_device_clipboard(controller, + GET_CLIPBOARD_COPY_KEY_COPY); } return; case SDLK_x: - if (control && !shift && !repeat) { - action_cut(controller, action); + if (control && !shift && !repeat && down) { + get_device_clipboard(controller, + GET_CLIPBOARD_COPY_KEY_CUT); } return; case SDLK_v: diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 5cd7056b..6fed2438 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -210,14 +210,18 @@ static void test_serialize_collapse_panels(void) { static void test_serialize_get_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + .get_clipboard = { + .copy_key = GET_CLIPBOARD_COPY_KEY_COPY, + }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); - assert(size == 1); + assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_GET_CLIPBOARD, + GET_CLIPBOARD_COPY_KEY_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 2cd80191..63ba0fa3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -20,6 +20,10 @@ public final class ControlMessage { public static final long SEQUENCE_INVALID = 0; + public static final int COPY_KEY_NONE = 0; + public static final int COPY_KEY_COPY = 1; + public static final int COPY_KEY_CUT = 2; + private int type; private String text; private int metaState; // KeyEvent.META_* @@ -31,6 +35,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; + private int copyKey; private boolean paste; private int repeat; private long sequence; @@ -82,6 +87,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createGetClipboard(int copyKey) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_GET_CLIPBOARD; + msg.copyKey = copyKey; + return msg; + } + public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; @@ -151,6 +163,10 @@ public final class ControlMessage { return vScroll; } + public int getCopyKey() { + return copyKey; + } + public boolean getPaste() { return paste; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 80931e94..f09ed26f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -13,6 +13,7 @@ public class ControlMessageReader { static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int GET_CLIPBOARD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k @@ -70,6 +71,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_BACK_OR_SCREEN_ON: msg = parseBackOrScreenOnEvent(); break; + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = parseGetClipboard(); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; @@ -79,7 +83,6 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: - case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; @@ -162,6 +165,14 @@ public class ControlMessageReader { return ControlMessage.createBackOrScreenOn(action); } + private ControlMessage parseGetClipboard() { + if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { + return null; + } + int copyKey = toUnsigned(buffer.get()); + return ControlMessage.createGetClipboard(copyKey); + } + 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 f853c85a..9246004a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -21,6 +21,7 @@ public class Controller { private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; + private final boolean clipboardAutosync; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -31,9 +32,10 @@ public class Controller { private boolean keepPowerModeOff; - public Controller(Device device, DesktopConnection connection) { + public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) { this.device = device; this.connection = connection; + this.clipboardAutosync = clipboardAutosync; initPointers(); sender = new DeviceMessageSender(connection); } @@ -114,10 +116,7 @@ public class Controller { Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = Device.getClipboardText(); - if (clipboardText != null) { - sender.pushClipboardText(clipboardText); - } + getClipboard(msg.getCopyKey()); break; case ControlMessage.TYPE_SET_CLIPBOARD: setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); @@ -276,6 +275,25 @@ public class Controller { return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } + private void getClipboard(int copyKey) { + // On Android >= 7, press the COPY or CUT key if requested + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; + // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one + device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + } + + // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in + // particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than + // copying an old clipboard content. + if (!clipboardAutosync) { + String clipboardText = Device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } + } + } + private boolean setClipboard(String text, boolean paste, long sequence) { boolean ok = device.setClipboardText(text); if (ok) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0f0abab0..fc31dada 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -74,7 +74,7 @@ public final class Server { Thread controllerThread = null; Thread deviceMessageSenderThread = null; if (options.getControl()) { - final Controller controller = new Controller(device, connection); + final Controller controller = new Controller(device, connection, options.getClipboardAutosync()); // asynchronous controllerThread = startController(controller); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 3b0b6c0d..5e79d4f0 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -219,6 +219,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + dos.writeByte(ControlMessage.COPY_KEY_COPY); byte[] packet = bos.toByteArray(); @@ -226,6 +227,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); } @Test