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 <https://github.com/Genymobile/scrcpy/issues/2228>
Refs #2817 <https://github.com/Genymobile/scrcpy/pull/2817>
PR #2834 <https://github.com/Genymobile/scrcpy/pull/2834>
This commit is contained in:
Romain Vimont 2021-11-29 09:30:57 +01:00
parent bfcb9d06c3
commit e2b3968c66
9 changed files with 102 additions and 26 deletions

View file

@ -55,6 +55,12 @@ static const char *const screen_power_mode_labels[] = {
"suspend", "suspend",
}; };
static const char *const copy_key_labels[] = {
"none",
"copy",
"cut",
};
static void static void
write_position(uint8_t *buf, const struct sc_position *position) { write_position(uint8_t *buf, const struct sc_position *position) {
buffer_write32be(&buf[0], position->point.x); 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: case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
buf[1] = msg->inject_keycode.action; buf[1] = msg->inject_keycode.action;
return 2; return 2;
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
buf[1] = msg->get_clipboard.copy_key;
return 2;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: { case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
buffer_write64be(&buf[1], msg->set_clipboard.sequence); buffer_write64be(&buf[1], msg->set_clipboard.sequence);
buf[9] = !!msg->set_clipboard.paste; 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_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE: case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data // no additional data
return 1; return 1;
@ -194,6 +202,10 @@ control_msg_log(const struct control_msg *msg) {
LOG_CMSG("back-or-screen-on %s", LOG_CMSG("back-or-screen-on %s",
KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); KEYEVENT_ACTION_LABEL(msg->inject_keycode.action));
break; 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: case CONTROL_MSG_TYPE_SET_CLIPBOARD:
LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"",
msg->set_clipboard.sequence, msg->set_clipboard.sequence,
@ -213,9 +225,6 @@ control_msg_log(const struct control_msg *msg) {
case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
LOG_CMSG("collapse panels"); LOG_CMSG("collapse panels");
break; break;
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
LOG_CMSG("get clipboard");
break;
case CONTROL_MSG_TYPE_ROTATE_DEVICE: case CONTROL_MSG_TYPE_ROTATE_DEVICE:
LOG_CMSG("rotate device"); LOG_CMSG("rotate device");
break; break;

View file

@ -41,6 +41,12 @@ enum screen_power_mode {
SCREEN_POWER_MODE_NORMAL = 2, 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 { struct control_msg {
enum control_msg_type type; enum control_msg_type type;
union { union {
@ -69,6 +75,9 @@ struct control_msg {
enum android_keyevent_action action; // action for the BACK key enum android_keyevent_action action; // action for the BACK key
// screen may only be turned on on ACTION_DOWN // screen may only be turned on on ACTION_DOWN
} back_or_screen_on; } back_or_screen_on;
struct {
enum get_clipboard_copy_key copy_key;
} get_clipboard;
struct { struct {
uint64_t sequence; uint64_t sequence;
char *text; // owned, to be freed by free() char *text; // owned, to be freed by free()

View file

@ -148,16 +148,6 @@ action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); 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 // turn the screen on if it was off, press BACK otherwise
// If the screen is off, it is turned on only on ACTION_DOWN // If the screen is off, it is turned on only on ACTION_DOWN
static void 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 static bool
set_device_clipboard(struct controller *controller, bool paste, set_device_clipboard(struct controller *controller, bool paste,
uint64_t sequence) { uint64_t sequence) {
@ -450,13 +455,15 @@ input_manager_process_key(struct input_manager *im,
} }
return; return;
case SDLK_c: case SDLK_c:
if (control && !shift && !repeat) { if (control && !shift && !repeat && down) {
action_copy(controller, action); get_device_clipboard(controller,
GET_CLIPBOARD_COPY_KEY_COPY);
} }
return; return;
case SDLK_x: case SDLK_x:
if (control && !shift && !repeat) { if (control && !shift && !repeat && down) {
action_cut(controller, action); get_device_clipboard(controller,
GET_CLIPBOARD_COPY_KEY_CUT);
} }
return; return;
case SDLK_v: case SDLK_v:

View file

@ -210,14 +210,18 @@ static void test_serialize_collapse_panels(void) {
static void test_serialize_get_clipboard(void) { static void test_serialize_get_clipboard(void) {
struct control_msg msg = { struct control_msg msg = {
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD, .type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
.get_clipboard = {
.copy_key = GET_CLIPBOARD_COPY_KEY_COPY,
},
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
size_t size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 2);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_GET_CLIPBOARD,
GET_CLIPBOARD_COPY_KEY_COPY,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }

View file

@ -20,6 +20,10 @@ public final class ControlMessage {
public static final long SEQUENCE_INVALID = 0; 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 int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* private int metaState; // KeyEvent.META_*
@ -31,6 +35,7 @@ public final class ControlMessage {
private Position position; private Position position;
private int hScroll; private int hScroll;
private int vScroll; private int vScroll;
private int copyKey;
private boolean paste; private boolean paste;
private int repeat; private int repeat;
private long sequence; private long sequence;
@ -82,6 +87,13 @@ public final class ControlMessage {
return msg; 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) { public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD; msg.type = TYPE_SET_CLIPBOARD;
@ -151,6 +163,10 @@ public final class ControlMessage {
return vScroll; return vScroll;
} }
public int getCopyKey() {
return copyKey;
}
public boolean getPaste() { public boolean getPaste() {
return paste; return paste;
} }

View file

@ -13,6 +13,7 @@ public class ControlMessageReader {
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_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; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
@ -70,6 +71,9 @@ public class ControlMessageReader {
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
msg = parseBackOrScreenOnEvent(); msg = parseBackOrScreenOnEvent();
break; break;
case ControlMessage.TYPE_GET_CLIPBOARD:
msg = parseGetClipboard();
break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard(); msg = parseSetClipboard();
break; break;
@ -79,7 +83,6 @@ public class ControlMessageReader {
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type); msg = ControlMessage.createEmpty(type);
break; break;
@ -162,6 +165,14 @@ public class ControlMessageReader {
return ControlMessage.createBackOrScreenOn(action); 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() { private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null; return null;

View file

@ -21,6 +21,7 @@ public class Controller {
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
private final boolean clipboardAutosync;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@ -31,9 +32,10 @@ public class Controller {
private boolean keepPowerModeOff; private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
this.clipboardAutosync = clipboardAutosync;
initPointers(); initPointers();
sender = new DeviceMessageSender(connection); sender = new DeviceMessageSender(connection);
} }
@ -114,10 +116,7 @@ public class Controller {
Device.collapsePanels(); Device.collapsePanels();
break; break;
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = Device.getClipboardText(); getClipboard(msg.getCopyKey());
if (clipboardText != null) {
sender.pushClipboardText(clipboardText);
}
break; break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
@ -276,6 +275,25 @@ public class Controller {
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); 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) { private boolean setClipboard(String text, boolean paste, long sequence) {
boolean ok = device.setClipboardText(text); boolean ok = device.setClipboardText(text);
if (ok) { if (ok) {

View file

@ -74,7 +74,7 @@ public final class Server {
Thread controllerThread = null; Thread controllerThread = null;
Thread deviceMessageSenderThread = null; Thread deviceMessageSenderThread = null;
if (options.getControl()) { if (options.getControl()) {
final Controller controller = new Controller(device, connection); final Controller controller = new Controller(device, connection, options.getClipboardAutosync());
// asynchronous // asynchronous
controllerThread = startController(controller); controllerThread = startController(controller);

View file

@ -219,6 +219,7 @@ public class ControlMessageReaderTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
dos.writeByte(ControlMessage.COPY_KEY_COPY);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -226,6 +227,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next(); ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
} }
@Test @Test