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
commit c13a24389c
12 changed files with 138 additions and 6 deletions

View file

@ -13,6 +13,7 @@ public final class ControlEvent {
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;
public static final int TYPE_SET_CLIPBOARD = 8;
private int type;
private String text;
@ -61,6 +62,13 @@ public final class ControlEvent {
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) {
ControlEvent event = new ControlEvent();
event.type = type;

View file

@ -13,11 +13,12 @@ public class ControlEventReader {
private static final int SCROLL_PAYLOAD_LENGTH = 20;
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 final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
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() {
// invariant: the buffer is always in "get" mode
@ -63,6 +64,9 @@ public class ControlEventReader {
case ControlEvent.TYPE_SCROLL:
controlEvent = parseScrollControlEvent();
break;
case ControlEvent.TYPE_SET_CLIPBOARD:
controlEvent = parseSetClipboardEvent();
break;
case ControlEvent.TYPE_BACK_OR_SCREEN_ON:
case ControlEvent.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL:
@ -132,6 +136,14 @@ public class ControlEventReader {
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) {
int x = buffer.getInt();
int y = buffer.getInt();

View file

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

View file

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

View file

@ -9,11 +9,13 @@ import java.lang.reflect.Method;
public class ClipboardManager {
private final IInterface manager;
private final Method getPrimaryClipMethod;
private final Method setPrimaryClipMethod;
public ClipboardManager(IInterface manager) {
this.manager = manager;
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
@ -30,4 +32,13 @@ public class ClipboardManager {
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());
}
@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
public void testMultiEvents() throws IOException {
ControlEventReader reader = new ControlEventReader();