mirror of
https://github.com/barry-ran/QtScrcpy.git
synced 2025-07-29 12:18:39 +00:00
add:实现剪切板功能
This commit is contained in:
parent
b44f78f19b
commit
b33b22bf16
27 changed files with 650 additions and 56 deletions
|
@ -9,10 +9,13 @@ public final class ControlEvent {
|
|||
public static final int TYPE_TEXT = 1;
|
||||
public static final int TYPE_MOUSE = 2;
|
||||
public static final int TYPE_SCROLL = 3;
|
||||
public static final int TYPE_TOUCH = 4;
|
||||
public static final int TYPE_BACK_OR_SCREEN_ON = 5;
|
||||
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 6;
|
||||
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 7;
|
||||
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;
|
||||
public static final int TYPE_SET_CLIPBOARD = 8;
|
||||
|
||||
public static final int TYPE_TOUCH = 9;
|
||||
|
||||
|
||||
private int type;
|
||||
|
@ -72,6 +75,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;
|
||||
|
|
|
@ -14,11 +14,12 @@ public class ControlEventReader {
|
|||
private static final int SCROLL_PAYLOAD_LENGTH = 16;
|
||||
|
||||
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
|
||||
|
@ -66,9 +67,13 @@ 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:
|
||||
case ControlEvent.TYPE_GET_CLIPBOARD:
|
||||
controlEvent = ControlEvent.createSimpleControlEvent(type);
|
||||
break;
|
||||
default:
|
||||
|
@ -94,8 +99,8 @@ public class ControlEventReader {
|
|||
return ControlEvent.createKeycodeControlEvent(action, keycode, metaState);
|
||||
}
|
||||
|
||||
private ControlEvent parseTextControlEvent() {
|
||||
if (buffer.remaining() < 1) {
|
||||
private String parseString() {
|
||||
if (buffer.remaining() < 2) {
|
||||
return null;
|
||||
}
|
||||
int len = toUnsigned(buffer.getShort());
|
||||
|
@ -103,7 +108,14 @@ public class ControlEventReader {
|
|||
return null;
|
||||
}
|
||||
buffer.get(textBuffer, 0, len);
|
||||
String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
||||
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private ControlEvent parseTextControlEvent() {
|
||||
String text = parseString();
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return ControlEvent.createTextControlEvent(text);
|
||||
}
|
||||
|
||||
|
@ -137,6 +149,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 = toUnsigned(buffer.getShort());
|
||||
int y = toUnsigned(buffer.getShort());
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.io.Closeable;
|
|||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class DesktopConnection implements Closeable {
|
||||
|
@ -21,14 +22,17 @@ public final class DesktopConnection implements Closeable {
|
|||
|
||||
private final LocalSocket controlSocket;
|
||||
private final InputStream controlInputStream;
|
||||
private final OutputStream controlOutputStream;
|
||||
|
||||
|
||||
private final ControlEventReader reader = new ControlEventReader();
|
||||
private final DeviceEventWriter writer = new DeviceEventWriter();
|
||||
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
||||
this.videoSocket = videoSocket;
|
||||
this.controlSocket = controlSocket;
|
||||
controlInputStream = controlSocket.getInputStream();
|
||||
controlOutputStream = controlSocket.getOutputStream();
|
||||
videoFd = videoSocket.getFileDescriptor();
|
||||
}
|
||||
|
||||
|
@ -109,4 +113,8 @@ public final class DesktopConnection implements Closeable {
|
|||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
public void sendDeviceEvent(DeviceEvent event) throws IOException {
|
||||
writer.writeTo(event, controlOutputStream);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -180,6 +180,18 @@ public final class Device {
|
|||
serviceManager.getStatusBarManager().collapsePanels();
|
||||
}
|
||||
|
||||
public String getClipboardText() {
|
||||
CharSequence s = serviceManager.getClipboardManager().getText();
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
27
server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java
Normal file
27
server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java
Normal file
|
@ -0,0 +1,27 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class DeviceEvent {
|
||||
|
||||
public static final int TYPE_GET_CLIPBOARD = 0;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
|
||||
private DeviceEvent() {
|
||||
}
|
||||
|
||||
public static DeviceEvent createGetClipboardEvent(String text) {
|
||||
DeviceEvent event = new DeviceEvent();
|
||||
event.type = TYPE_GET_CLIPBOARD;
|
||||
event.text = text;
|
||||
return event;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DeviceEventWriter {
|
||||
|
||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
||||
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3;
|
||||
|
||||
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
public void writeTo(DeviceEvent event, OutputStream output) throws IOException {
|
||||
buffer.clear();
|
||||
buffer.put((byte) DeviceEvent.TYPE_GET_CLIPBOARD);
|
||||
switch (event.getType()) {
|
||||
case DeviceEvent.TYPE_GET_CLIPBOARD:
|
||||
String text = event.getText();
|
||||
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
buffer.putShort((short) len);
|
||||
buffer.put(raw, 0, len);
|
||||
output.write(rawBuffer, 0, buffer.position());
|
||||
break;
|
||||
default:
|
||||
Ln.w("Unknown device event: " + event.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,11 +13,11 @@ import android.view.MotionEvent;
|
|||
import java.io.IOException;
|
||||
import java.util.Vector;
|
||||
|
||||
|
||||
public class EventController {
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final EventSender sender;
|
||||
|
||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||
|
||||
|
@ -28,6 +28,7 @@ public class EventController {
|
|||
public EventController(Device device, DesktopConnection connection) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
sender = new EventSender(connection);
|
||||
}
|
||||
|
||||
private int getPointer(int id) {
|
||||
|
@ -97,6 +98,10 @@ public class EventController {
|
|||
}
|
||||
}
|
||||
|
||||
public EventSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public void control() throws IOException {
|
||||
// on start, turn screen on
|
||||
turnScreenOn();
|
||||
|
@ -133,6 +138,13 @@ 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;
|
||||
case ControlEvent.TYPE_SET_CLIPBOARD:
|
||||
device.setClipboardText(controlEvent.getText());
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
@ -144,7 +156,7 @@ public class EventController {
|
|||
|
||||
private boolean injectChar(char c) {
|
||||
String decomposed = KeyComposition.decompose(c);
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c};
|
||||
KeyEvent[] events = charMap.getEvents(chars);
|
||||
if (events == null) {
|
||||
return false;
|
||||
|
|
34
server/src/main/java/com/genymobile/scrcpy/EventSender.java
Normal file
34
server/src/main/java/com/genymobile/scrcpy/EventSender.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class EventSender {
|
||||
|
||||
private final DesktopConnection connection;
|
||||
|
||||
private String clipboardText;
|
||||
|
||||
public EventSender(DesktopConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public synchronized void pushClipboardText(String text) {
|
||||
clipboardText = text;
|
||||
notify();
|
||||
}
|
||||
|
||||
public void loop() throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
String text;
|
||||
synchronized (this) {
|
||||
while (clipboardText == null) {
|
||||
wait();
|
||||
}
|
||||
text = clipboardText;
|
||||
clipboardText = null;
|
||||
}
|
||||
DeviceEvent event = DeviceEvent.createGetClipboardEvent(text);
|
||||
connection.sendDeviceEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,8 +20,11 @@ public final class Server {
|
|||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
|
||||
|
||||
EventController controller = new EventController(device, connection);
|
||||
|
||||
// asynchronous
|
||||
startEventController(device, connection);
|
||||
startEventController(controller);
|
||||
startEventSender(controller.getSender());
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
|
@ -33,12 +36,12 @@ public final class Server {
|
|||
}
|
||||
}
|
||||
|
||||
private static void startEventController(final Device device, final DesktopConnection connection) {
|
||||
private static void startEventController(final EventController controller) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
new EventController(device, connection).control();
|
||||
controller.control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Event controller stopped");
|
||||
|
@ -47,6 +50,20 @@ public final class Server {
|
|||
}).start();
|
||||
}
|
||||
|
||||
private static void startEventSender(final EventSender sender) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Event sender stopped");
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length != 5) {
|
||||
|
|
23
server/src/main/java/com/genymobile/scrcpy/StringUtils.java
Normal file
23
server/src/main/java/com/genymobile/scrcpy/StringUtils.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class StringUtils {
|
||||
private StringUtils() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
|
||||
int len = utf8.length;
|
||||
if (len <= maxLength) {
|
||||
return len;
|
||||
}
|
||||
len = maxLength;
|
||||
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
|
||||
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
|
||||
// the next byte is not the start of a new UTF-8 codepoint
|
||||
// so if we would cut there, the character would be truncated
|
||||
len--;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue