Implement computer-to-device clipboard copy

It was already possible to _paste_ (with Ctrl+v) the content of the
computer clipboard on the device. Technically, it injects a sequence of
events to generate the text.

Add a new feature (Ctrl+Shift+v) to copy to the device clipboard
instead, without injecting the content. Contrary to events injection,
this preserves the UTF-8 content exactly, so the text is not broken by
special characters.

<https://github.com/Genymobile/scrcpy/issues/413>
This commit is contained in:
Romain Vimont 2019-05-30 20:25:23 +02:00
parent 2322069656
commit c13a24389c
12 changed files with 138 additions and 6 deletions

View file

@ -289,6 +289,7 @@ you are interested, see [issue 14].
| collapse notification panel | `Ctrl`+`Shift`+`n` | | collapse notification panel | `Ctrl`+`Shift`+`n` |
| copy device clipboard to computer | `Ctrl`+`c` | | copy device clipboard to computer | `Ctrl`+`c` |
| paste computer clipboard to device | `Ctrl`+`v` | | paste computer clipboard to device | `Ctrl`+`v` |
| copy computer clipboard to device | `Ctrl`+`Shift+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._

View file

@ -47,6 +47,12 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) {
buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll); buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll);
buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll); buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll);
return 21; return 21;
case CONTROL_EVENT_TYPE_SET_CLIPBOARD: {
size_t len = write_string(event->text_event.text,
CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[1]);
return 1 + len;
}
case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON: case CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL:
@ -61,7 +67,15 @@ control_event_serialize(const struct control_event *event, unsigned char *buf) {
void void
control_event_destroy(struct control_event *event) { control_event_destroy(struct control_event *event) {
if (event->type == CONTROL_EVENT_TYPE_TEXT) { switch (event->type) {
SDL_free(event->text_event.text); case CONTROL_EVENT_TYPE_TEXT:
SDL_free(event->text_event.text);
break;
case CONTROL_EVENT_TYPE_SET_CLIPBOARD:
SDL_free(event->set_clipboard_event.text);
break;
default:
// do nothing
break;
} }
} }

View file

@ -10,7 +10,9 @@
#include "common.h" #include "common.h"
#define CONTROL_EVENT_TEXT_MAX_LENGTH 300 #define CONTROL_EVENT_TEXT_MAX_LENGTH 300
#define CONTROL_EVENT_SERIALIZED_MAX_SIZE (3 + CONTROL_EVENT_TEXT_MAX_LENGTH) #define CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define CONTROL_EVENT_SERIALIZED_MAX_SIZE \
(3 + CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH)
enum control_event_type { enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE, CONTROL_EVENT_TYPE_KEYCODE,
@ -21,6 +23,7 @@ enum control_event_type {
CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL, CONTROL_EVENT_TYPE_EXPAND_NOTIFICATION_PANEL,
CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_EVENT_TYPE_COLLAPSE_NOTIFICATION_PANEL,
CONTROL_EVENT_TYPE_GET_CLIPBOARD, CONTROL_EVENT_TYPE_GET_CLIPBOARD,
CONTROL_EVENT_TYPE_SET_CLIPBOARD,
}; };
struct control_event { struct control_event {
@ -44,6 +47,9 @@ struct control_event {
int32_t hscroll; int32_t hscroll;
int32_t vscroll; int32_t vscroll;
} scroll_event; } scroll_event;
struct {
char *text; // owned, to be freed by SDL_free()
} set_clipboard_event;
}; };
}; };

View file

@ -136,6 +136,29 @@ request_device_clipboard(struct controller *controller) {
} }
} }
static void
set_device_clipboard(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Cannot get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
// empty text
SDL_free(text);
return;
}
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_SET_CLIPBOARD;
control_event.set_clipboard_event.text = text;
if (!controller_push_event(controller, &control_event)) {
SDL_free(text);
LOGW("Cannot send clipboard paste event");
}
}
static void static void
switch_fps_counter_state(struct video_buffer *vb) { switch_fps_counter_state(struct video_buffer *vb) {
mutex_lock(vb->mutex); mutex_lock(vb->mutex);
@ -267,9 +290,15 @@ input_manager_process_key(struct input_manager *input_manager,
} }
return; return;
case SDLK_v: case SDLK_v:
if (control && ctrl && !meta && !shift && !repeat if (control && ctrl && !meta && !repeat
&& event->type == SDL_KEYDOWN) { && event->type == SDL_KEYDOWN) {
clipboard_paste(input_manager->controller); if (shift) {
// store the text in the device clipboard
set_device_clipboard(input_manager->controller);
} else {
// inject the text as input events
clipboard_paste(input_manager->controller);
}
} }
return; return;
case SDLK_f: case SDLK_f:

View file

@ -143,6 +143,9 @@ static void usage(const char *arg0) {
" Ctrl+v\n" " Ctrl+v\n"
" paste computer clipboard to device\n" " paste computer clipboard to device\n"
"\n" "\n"
" Ctrl+Shift+v\n"
" copy computer clipboard to device\n"
"\n"
" Ctrl+i\n" " Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n" " enable/disable FPS counter (print frames/second in logs)\n"
"\n" "\n"

View file

@ -193,6 +193,26 @@ static void test_serialize_get_clipboard_event(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_set_clipboard_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_SET_CLIPBOARD,
.text_event = {
.text = "hello, world!",
},
};
unsigned char buf[CONTROL_EVENT_SERIALIZED_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 16);
const unsigned char expected[] = {
0x08, // CONTROL_EVENT_TYPE_SET_CLIPBOARD
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) { int main(void) {
test_serialize_keycode_event(); test_serialize_keycode_event();
test_serialize_text_event(); test_serialize_text_event();
@ -203,5 +223,6 @@ int main(void) {
test_serialize_expand_notification_panel_event(); test_serialize_expand_notification_panel_event();
test_serialize_collapse_notification_panel_event(); test_serialize_collapse_notification_panel_event();
test_serialize_get_clipboard_event(); test_serialize_get_clipboard_event();
test_serialize_set_clipboard_event();
return 0; return 0;
} }

View file

@ -13,6 +13,7 @@ public final class ControlEvent {
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8;
private int type; private int type;
private String text; private String text;
@ -61,6 +62,13 @@ public final class ControlEvent {
return event; return event;
} }
public static ControlEvent createSetClipboardControlEvent(String text) {
ControlEvent event = new ControlEvent();
event.type = TYPE_SET_CLIPBOARD;
event.text = text;
return event;
}
public static ControlEvent createSimpleControlEvent(int type) { public static ControlEvent createSimpleControlEvent(int type) {
ControlEvent event = new ControlEvent(); ControlEvent event = new ControlEvent();
event.type = type; event.type = type;

View file

@ -13,11 +13,12 @@ public class ControlEventReader {
private static final int SCROLL_PAYLOAD_LENGTH = 20; private static final int SCROLL_PAYLOAD_LENGTH = 20;
public static final int TEXT_MAX_LENGTH = 300; public static final int TEXT_MAX_LENGTH = 300;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int RAW_BUFFER_SIZE = 1024; private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH]; private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlEventReader() { public ControlEventReader() {
// invariant: the buffer is always in "get" mode // invariant: the buffer is always in "get" mode
@ -63,6 +64,9 @@ public class ControlEventReader {
case ControlEvent.TYPE_SCROLL: case ControlEvent.TYPE_SCROLL:
controlEvent = parseScrollControlEvent(); controlEvent = parseScrollControlEvent();
break; break;
case ControlEvent.TYPE_SET_CLIPBOARD:
controlEvent = parseSetClipboardEvent();
break;
case ControlEvent.TYPE_BACK_OR_SCREEN_ON: case ControlEvent.TYPE_BACK_OR_SCREEN_ON:
case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL:
@ -132,6 +136,14 @@ public class ControlEventReader {
return ControlEvent.createScrollControlEvent(position, hScroll, vScroll); return ControlEvent.createScrollControlEvent(position, hScroll, vScroll);
} }
private ControlEvent parseSetClipboardEvent() {
String text = parseString();
if (text == null) {
return null;
}
return ControlEvent.createSetClipboardControlEvent(text);
}
private static Position readPosition(ByteBuffer buffer) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = buffer.getInt();
int y = buffer.getInt(); int y = buffer.getInt();

View file

@ -147,6 +147,10 @@ public final class Device {
return s.toString(); return s.toString();
} }
public void setClipboardText(String text) {
serviceManager.getClipboardManager().setText(text);
}
static Rect flipRect(Rect crop) { static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right); return new Rect(crop.top, crop.left, crop.bottom, crop.right);
} }

View file

@ -94,6 +94,9 @@ public class EventController {
String clipboardText = device.getClipboardText(); String clipboardText = device.getClipboardText();
sender.pushClipboardText(clipboardText); sender.pushClipboardText(clipboardText);
break; break;
case ControlEvent.TYPE_SET_CLIPBOARD:
device.setClipboardText(controlEvent.getText());
break;
default: default:
// do nothing // do nothing
} }

View file

@ -9,11 +9,13 @@ import java.lang.reflect.Method;
public class ClipboardManager { public class ClipboardManager {
private final IInterface manager; private final IInterface manager;
private final Method getPrimaryClipMethod; private final Method getPrimaryClipMethod;
private final Method setPrimaryClipMethod;
public ClipboardManager(IInterface manager) { public ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
try { try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@ -30,4 +32,13 @@ public class ClipboardManager {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
public void setText(CharSequence text) {
ClipData clipData = ClipData.newPlainText(null, text);
try {
setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell");
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
} }

View file

@ -190,6 +190,26 @@ public class ControlEventReaderTest {
Assert.assertEquals(ControlEvent.TYPE_GET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlEvent.TYPE_GET_CLIPBOARD, event.getType());
} }
@Test
public void testParseSetClipboardEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_SET_CLIPBOARD);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals("testé", event.getText());
}
@Test @Test
public void testMultiEvents() throws IOException { public void testMultiEvents() throws IOException {
ControlEventReader reader = new ControlEventReader(); ControlEventReader reader = new ControlEventReader();