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 <https://github.com/Genymobile/scrcpy/issues/145>
This commit is contained in:
parent
3149e2cf4a
commit
63c078ee6c
13 changed files with 109 additions and 0 deletions
|
@ -287,6 +287,7 @@ you are interested, see [issue 14].
|
||||||
| turn screen on | _Right-click²_ |
|
| turn screen on | _Right-click²_ |
|
||||||
| expand notification panel | `Ctrl`+`n` |
|
| expand notification panel | `Ctrl`+`n` |
|
||||||
| collapse notification panel | `Ctrl`+`Shift`+`n` |
|
| collapse notification panel | `Ctrl`+`Shift`+`n` |
|
||||||
|
| copy device clipboard to computer | `Ctrl`+`c` |
|
||||||
| paste computer clipboard to device | `Ctrl`+`v` |
|
| paste computer clipboard to device | `Ctrl`+`v` |
|
||||||
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
|
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
|
||||||
|
|
||||||
|
|
|
@ -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_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:
|
||||||
|
case CONTROL_EVENT_TYPE_GET_CLIPBOARD:
|
||||||
// no additional data
|
// no additional data
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -20,6 +20,7 @@ enum control_event_type {
|
||||||
CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON,
|
CONTROL_EVENT_TYPE_BACK_OR_SCREEN_ON,
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct control_event {
|
struct control_event {
|
||||||
|
|
|
@ -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
|
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);
|
||||||
|
@ -250,6 +260,12 @@ input_manager_process_key(struct input_manager *input_manager,
|
||||||
action_volume_up(input_manager->controller, action);
|
action_volume_up(input_manager->controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case SDLK_c:
|
||||||
|
if (control && ctrl && !meta && !shift && !repeat
|
||||||
|
&& event->type == SDL_KEYDOWN) {
|
||||||
|
request_device_clipboard(input_manager->controller);
|
||||||
|
}
|
||||||
|
return;
|
||||||
case SDLK_v:
|
case SDLK_v:
|
||||||
if (control && ctrl && !meta && !shift && !repeat
|
if (control && ctrl && !meta && !shift && !repeat
|
||||||
&& event->type == SDL_KEYDOWN) {
|
&& event->type == SDL_KEYDOWN) {
|
||||||
|
|
|
@ -137,6 +137,9 @@ static void usage(const char *arg0) {
|
||||||
" Ctrl+Shift+n\n"
|
" Ctrl+Shift+n\n"
|
||||||
" collapse notification panel\n"
|
" collapse notification panel\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" Ctrl+c\n"
|
||||||
|
" copy device clipboard to computer\n"
|
||||||
|
"\n"
|
||||||
" Ctrl+v\n"
|
" Ctrl+v\n"
|
||||||
" paste computer clipboard to device\n"
|
" paste computer clipboard to device\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
|
@ -178,6 +178,21 @@ static void test_serialize_collapse_notification_panel_event(void) {
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
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) {
|
int main(void) {
|
||||||
test_serialize_keycode_event();
|
test_serialize_keycode_event();
|
||||||
test_serialize_text_event();
|
test_serialize_text_event();
|
||||||
|
@ -187,5 +202,6 @@ int main(void) {
|
||||||
test_serialize_back_or_screen_on_event();
|
test_serialize_back_or_screen_on_event();
|
||||||
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();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ public final class ControlEvent {
|
||||||
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
|
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
|
||||||
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;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
|
|
|
@ -66,6 +66,7 @@ public class ControlEventReader {
|
||||||
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:
|
||||||
|
case ControlEvent.TYPE_GET_CLIPBOARD:
|
||||||
controlEvent = ControlEvent.createSimpleControlEvent(type);
|
controlEvent = ControlEvent.createSimpleControlEvent(type);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -139,6 +139,14 @@ public final class Device {
|
||||||
serviceManager.getStatusBarManager().collapsePanels();
|
serviceManager.getStatusBarManager().collapsePanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClipboardText() {
|
||||||
|
CharSequence s = serviceManager.getClipboardManager().getText();
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return s.toString();
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,10 @@ public class EventController {
|
||||||
case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
device.collapsePanels();
|
device.collapsePanels();
|
||||||
break;
|
break;
|
||||||
|
case ControlEvent.TYPE_GET_CLIPBOARD:
|
||||||
|
String clipboardText = device.getClipboardText();
|
||||||
|
sender.pushClipboardText(clipboardText);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ public final class ServiceManager {
|
||||||
private InputManager inputManager;
|
private InputManager inputManager;
|
||||||
private PowerManager powerManager;
|
private PowerManager powerManager;
|
||||||
private StatusBarManager statusBarManager;
|
private StatusBarManager statusBarManager;
|
||||||
|
private ClipboardManager clipboardManager;
|
||||||
|
|
||||||
public ServiceManager() {
|
public ServiceManager() {
|
||||||
try {
|
try {
|
||||||
|
@ -68,4 +69,11 @@ public final class ServiceManager {
|
||||||
}
|
}
|
||||||
return statusBarManager;
|
return statusBarManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClipboardManager getClipboardManager() {
|
||||||
|
if (clipboardManager == null) {
|
||||||
|
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
|
||||||
|
}
|
||||||
|
return clipboardManager;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,6 +174,22 @@ public class ControlEventReaderTest {
|
||||||
Assert.assertEquals(ControlEvent.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
|
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
|
@Test
|
||||||
public void testMultiEvents() throws IOException {
|
public void testMultiEvents() throws IOException {
|
||||||
ControlEventReader reader = new ControlEventReader();
|
ControlEventReader reader = new ControlEventReader();
|
||||||
|
|
Loading…
Reference in a new issue