From 63c078ee6ca19c4a2b3d84b04068220ef425aaf4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2019 15:23:01 +0200 Subject: [PATCH] Implement device-to-computer clipboard copy On Ctrl+C: - the client sends a GET_CLIPBOARD command to the device; - the device retrieve its current clipboard text and sends it in a GET_CLIPBOARD device event; - the client sets this text as the system clipboard text, so that it can be pasted in another application. Fixes --- README.md | 1 + app/src/control_event.c | 1 + app/src/control_event.h | 1 + app/src/input_manager.c | 16 +++++++++ app/src/main.c | 3 ++ app/tests/test_control_event_serialize.c | 16 +++++++++ .../com/genymobile/scrcpy/ControlEvent.java | 1 + .../genymobile/scrcpy/ControlEventReader.java | 1 + .../java/com/genymobile/scrcpy/Device.java | 8 +++++ .../genymobile/scrcpy/EventController.java | 4 +++ .../scrcpy/wrappers/ClipboardManager.java | 33 +++++++++++++++++++ .../scrcpy/wrappers/ServiceManager.java | 8 +++++ .../scrcpy/ControlEventReaderTest.java | 16 +++++++++ 13 files changed, 109 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java diff --git a/README.md b/README.md index 5daafb2e..cef20b10 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ you are interested, see [issue 14]. | turn screen on | _Right-click²_ | | expand notification panel | `Ctrl`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` | + | copy device clipboard to computer | `Ctrl`+`c` | | paste computer clipboard to device | `Ctrl`+`v` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | diff --git a/app/src/control_event.c b/app/src/control_event.c index 80326c58..bd42ddb1 100644 --- a/app/src/control_event.c +++ b/app/src/control_event.c @@ -50,6 +50,7 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) { case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON: case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_EVENT_TYPE_GET_CLIPBOARD: // no additional data return 1; default: diff --git a/app/src/control_event.h b/app/src/control_event.h index a720e7bc..cc5afa70 100644 --- a/app/src/control_event.h +++ b/app/src/control_event.h @@ -20,6 +20,7 @@ enum control_event_type { CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON, CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_EVENT_TYPE_GET_CLIPBOARD, }; struct control_event { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9567d21f..3940f4f8 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -126,6 +126,16 @@ collapse_notification_panel(struct controller *controller) { } } +static void +request_device_clipboard(struct controller *controller) { + struct control_event control_event; + control_event.type = CONTROL_EVENT_TYPE_GET_CLIPBOARD; + + if (!controller_push_event(controller, &control_event)) { + LOGW("Cannot get device clipboard"); + } +} + static void switch_fps_counter_state(struct video_buffer *vb) { mutex_lock(vb->mutex); @@ -250,6 +260,12 @@ input_manager_process_key(struct input_manager *input_manager, action_volume_up(input_manager->controller, action); } return; + case SDLK_c: + if (control && ctrl && !meta && !shift && !repeat + && event->type == SDL_KEYDOWN) { + request_device_clipboard(input_manager->controller); + } + return; case SDLK_v: if (control && ctrl && !meta && !shift && !repeat && event->type == SDL_KEYDOWN) { diff --git a/app/src/main.c b/app/src/main.c index fe93673f..8b415909 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -137,6 +137,9 @@ static void usage(const char *arg0) { " Ctrl+Shift+n\n" " collapse notification panel\n" "\n" + " Ctrl+c\n" + " copy device clipboard to computer\n" + "\n" " Ctrl+v\n" " paste computer clipboard to device\n" "\n" diff --git a/app/tests/test_control_event_serialize.c b/app/tests/test_control_event_serialize.c index 6a7720bc..038d913d 100644 --- a/app/tests/test_control_event_serialize.c +++ b/app/tests/test_control_event_serialize.c @@ -178,6 +178,21 @@ static void test_serialize_collapse_notification_panel_event(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_get_clipboard_event(void) { + struct control_event event = { + .type = CONTROL_EVENT_TYPE_GET_CLIPBOARD, + }; + + unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE]; + int size = control_event_serialize(&event, buf); + assert(size == 1); + + const unsigned char expected[] = { + 0x07, // CONTROL_EVENT_TYPE_GET_CLIPBOARD + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_keycode_event(); test_serialize_text_event(); @@ -187,5 +202,6 @@ int main(void) { test_serialize_back_or_screen_on_event(); test_serialize_expand_notification_panel_event(); test_serialize_collapse_notification_panel_event(); + test_serialize_get_clipboard_event(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java index 6318c82b..1784c953 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java @@ -12,6 +12,7 @@ public final class ControlEvent { public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_GET_CLIPBOARD = 7; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index 0fdf51c5..b316c34a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -66,6 +66,7 @@ public class ControlEventReader { case ControlEvent.TYPE_BACK_OR_SCREEN_ON: case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlEvent.TYPE_GET_CLIPBOARD: controlEvent = ControlEvent.createSimpleControlEvent(type); break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 57496363..c373d390 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -139,6 +139,14 @@ public final class Device { serviceManager.getStatusBarManager().collapsePanels(); } + public String getClipboardText() { + CharSequence s = serviceManager.getClipboardManager().getText(); + if (s == null) { + return null; + } + return s.toString(); + } + static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 4bda0f3d..bec25103 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -90,6 +90,10 @@ public class EventController { case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: device.collapsePanels(); break; + case ControlEvent.TYPE_GET_CLIPBOARD: + String clipboardText = device.getClipboardText(); + sender.pushClipboardText(clipboardText); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java new file mode 100644 index 00000000..60ce5f13 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy.wrappers; + +import android.content.ClipData; +import android.os.IInterface; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ClipboardManager { + private final IInterface manager; + private final Method getPrimaryClipMethod; + + public ClipboardManager(IInterface manager) { + this.manager = manager; + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + public CharSequence getText() { + try { + ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + if (clipData == null || clipData.getItemCount() == 0) { + return null; + } + return clipData.getItemAt(0).getText(); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new AssertionError(e); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 3bcdc0e6..0b625c92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -15,6 +15,7 @@ public final class ServiceManager { private InputManager inputManager; private PowerManager powerManager; private StatusBarManager statusBarManager; + private ClipboardManager clipboardManager; public ServiceManager() { try { @@ -68,4 +69,11 @@ public final class ServiceManager { } return statusBarManager; } + + public ClipboardManager getClipboardManager() { + if (clipboardManager == null) { + clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + } + return clipboardManager; + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java index 8f0724ff..d19d1bc6 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlEventReaderTest.java @@ -174,6 +174,22 @@ public class ControlEventReaderTest { Assert.assertEquals(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); } + @Test + public void testParseGetClipboardEvent() throws IOException { + ControlEventReader reader = new ControlEventReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlEvent.TYPE_GET_CLIPBOARD); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlEvent event = reader.next(); + + Assert.assertEquals(ControlEvent.TYPE_GET_CLIPBOARD, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlEventReader reader = new ControlEventReader();