diff --git a/QtScrcpy/inputcontrol/controlevent.cpp b/QtScrcpy/inputcontrol/controlevent.cpp index 1de03b4..0e4c1da 100644 --- a/QtScrcpy/inputcontrol/controlevent.cpp +++ b/QtScrcpy/inputcontrol/controlevent.cpp @@ -1,6 +1,7 @@ #include #include "controlevent.h" +#include "bufferutil.h" ControlEvent::ControlEvent(ControlEventType controlEventType) : QScrcpyEvent(Control) @@ -8,6 +9,19 @@ ControlEvent::ControlEvent(ControlEventType controlEventType) m_data.type = controlEventType; } +ControlEvent::~ControlEvent() +{ + if (CET_SET_CLIPBOARD == m_data.type + && Q_NULLPTR != m_data.setClipboardEvent.text) { + delete m_data.setClipboardEvent.text; + m_data.setClipboardEvent.text = Q_NULLPTR; + } else if (CET_TEXT == m_data.type + && Q_NULLPTR != m_data.textEvent.text){ + delete m_data.textEvent.text; + m_data.textEvent.text = Q_NULLPTR; + } +} + void ControlEvent::setKeycodeEventData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate) { m_data.keycodeEvent.action = action; @@ -15,16 +29,17 @@ void ControlEvent::setKeycodeEventData(AndroidKeyeventAction action, AndroidKeyc m_data.keycodeEvent.metastate = metastate; } -void ControlEvent::setTextEventData(QString text) +void ControlEvent::setTextEventData(QString& text) { // write length (2 byte) + string (non nul-terminated) - if (TEXT_MAX_CHARACTER_LENGTH < text.length()) { + if (CONTROL_EVENT_TEXT_MAX_LENGTH < text.length()) { // injecting a text takes time, so limit the text length - text = text.left(TEXT_MAX_CHARACTER_LENGTH); + text = text.left(CONTROL_EVENT_TEXT_MAX_LENGTH); } QByteArray tmp = text.toUtf8(); - memset(m_data.textEvent.text, 0, sizeof (m_data.textEvent.text)); + m_data.textEvent.text = new char[tmp.length() + 1]; memcpy(m_data.textEvent.text, tmp.data(), tmp.length()); + m_data.textEvent.text[tmp.length()] = '\0'; } void ControlEvent::setMouseEventData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position) @@ -48,26 +63,27 @@ void ControlEvent::setScrollEventData(QRect position, qint32 hScroll, qint32 vSc m_data.scrollEvent.vScroll = vScroll; } -void ControlEvent::write32(QBuffer &buffer, quint32 value) +void ControlEvent::setSetClipboardEventData(QString &text) { - buffer.putChar(value >> 24); - buffer.putChar(value >> 16); - buffer.putChar(value >> 8); - buffer.putChar(value); -} + if (text.isEmpty()) { + return; + } + if (CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH < text.length()) { + text = text.left(CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH); + } -void ControlEvent::write16(QBuffer &buffer, quint32 value) -{ - buffer.putChar(value >> 8); - buffer.putChar(value); + QByteArray tmp = text.toUtf8(); + m_data.setClipboardEvent.text = new char[tmp.length() + 1]; + memcpy(m_data.setClipboardEvent.text, tmp.data(), tmp.length()); + m_data.setClipboardEvent.text[tmp.length()] = '\0'; } void ControlEvent::writePosition(QBuffer &buffer, const QRect& value) { - write16(buffer, value.left()); - write16(buffer, value.top()); - write16(buffer, value.width()); - write16(buffer, value.height()); + BufferUtil::write16(buffer, value.left()); + BufferUtil::write16(buffer, value.top()); + BufferUtil::write16(buffer, value.width()); + BufferUtil::write16(buffer, value.height()); } QByteArray ControlEvent::serializeData() @@ -80,18 +96,16 @@ QByteArray ControlEvent::serializeData() switch (m_data.type) { case CET_KEYCODE: buffer.putChar(m_data.keycodeEvent.action); - write32(buffer, m_data.keycodeEvent.keycode); - write32(buffer, m_data.keycodeEvent.metastate); + BufferUtil::write32(buffer, m_data.keycodeEvent.keycode); + BufferUtil::write32(buffer, m_data.keycodeEvent.metastate); break; case CET_TEXT: - { - write16(buffer, strlen(m_data.textEvent.text)); + BufferUtil::write16(buffer, strlen(m_data.textEvent.text)); buffer.write(m_data.textEvent.text, strlen(m_data.textEvent.text)); - } break; case CET_MOUSE: buffer.putChar(m_data.mouseEvent.action); - write32(buffer, m_data.mouseEvent.buttons); + BufferUtil::write32(buffer, m_data.mouseEvent.buttons); writePosition(buffer, m_data.mouseEvent.position); break; case CET_TOUCH: @@ -101,12 +115,17 @@ QByteArray ControlEvent::serializeData() break; case CET_SCROLL: writePosition(buffer, m_data.scrollEvent.position); - write32(buffer, m_data.scrollEvent.hScroll); - write32(buffer, m_data.scrollEvent.vScroll); + BufferUtil::write32(buffer, m_data.scrollEvent.hScroll); + BufferUtil::write32(buffer, m_data.scrollEvent.vScroll); + break; + case CET_SET_CLIPBOARD: + BufferUtil::write16(buffer, strlen(m_data.setClipboardEvent.text)); + buffer.write(m_data.setClipboardEvent.text, strlen(m_data.setClipboardEvent.text)); break; case CET_BACK_OR_SCREEN_ON: case CET_EXPAND_NOTIFICATION_PANEL: - case CET_COLLAPSE_NOTIFICATION_PANEL: + case CET_COLLAPSE_NOTIFICATION_PANEL: + case CET_GET_CLIPBOARD: break; default: qDebug() << "Unknown event type:" << m_data.type; diff --git a/QtScrcpy/inputcontrol/controlevent.h b/QtScrcpy/inputcontrol/controlevent.h index 02688e1..67f99a0 100644 --- a/QtScrcpy/inputcontrol/controlevent.h +++ b/QtScrcpy/inputcontrol/controlevent.h @@ -9,43 +9,48 @@ #include "input.h" #include "keycodes.h" -#define TEXT_MAX_CHARACTER_LENGTH 300 +#define CONTROL_EVENT_TEXT_MAX_LENGTH 300 +#define CONTROL_EVENT_CLIPBOARD_TEXT_MAX_LENGTH 4093 // ControlEvent class ControlEvent : public QScrcpyEvent { public: enum ControlEventType { + CET_NULL = -1, CET_KEYCODE = 0, CET_TEXT, - CET_MOUSE, - CET_SCROLL, - CET_TOUCH, + CET_MOUSE, + CET_SCROLL, CET_BACK_OR_SCREEN_ON, CET_EXPAND_NOTIFICATION_PANEL, CET_COLLAPSE_NOTIFICATION_PANEL, + CET_GET_CLIPBOARD, + CET_SET_CLIPBOARD, + + CET_TOUCH, }; ControlEvent(ControlEventType controlEventType); + virtual ~ControlEvent(); void setKeycodeEventData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate); - void setTextEventData(QString text); + void setTextEventData(QString& text); void setMouseEventData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position); // id 代表一个触摸点,最多支持10个触摸点[0,9] // action 只能是AMOTION_EVENT_ACTION_DOWN,AMOTION_EVENT_ACTION_UP,AMOTION_EVENT_ACTION_MOVE // position action动作对应的位置 void setTouchEventData(quint32 id, AndroidMotioneventAction action, QRect position); void setScrollEventData(QRect position, qint32 hScroll, qint32 vScroll); + void setSetClipboardEventData(QString& text); QByteArray serializeData(); -private: - void write32(QBuffer& buffer, quint32 value); - void write16(QBuffer& buffer, quint32 value); +private: void writePosition(QBuffer& buffer, const QRect& value); private: struct ControlEventData { - ControlEventType type; + ControlEventType type = CET_NULL; union { struct { AndroidKeyeventAction action; @@ -53,7 +58,7 @@ private: AndroidMetastate metastate; } keycodeEvent; struct { - char text[TEXT_MAX_CHARACTER_LENGTH + 1]; + char* text = Q_NULLPTR; } textEvent; struct { AndroidMotioneventAction action; @@ -70,6 +75,9 @@ private: qint32 hScroll; qint32 vScroll; } scrollEvent; + struct { + char *text = Q_NULLPTR; + } setClipboardEvent; }; ControlEventData(){} diff --git a/QtScrcpy/inputcontrol/controller.cpp b/QtScrcpy/inputcontrol/controller.cpp index 53f030a..dcd867e 100644 --- a/QtScrcpy/inputcontrol/controller.cpp +++ b/QtScrcpy/inputcontrol/controller.cpp @@ -3,10 +3,11 @@ #include "controller.h" #include "videosocket.h" #include "controlevent.h" +#include "receiver.h" Controller::Controller(QObject* parent) : QObject(parent) { - + m_receiver = new Receiver(this); } Controller::~Controller() @@ -16,7 +17,16 @@ Controller::~Controller() void Controller::setControlSocket(QTcpSocket* controlSocket) { + if (m_controlSocket || !controlSocket) { + return; + } m_controlSocket = controlSocket; + connect(controlSocket, &QTcpSocket::readyRead, m_receiver, &Receiver::onReadyRead); +} + +QTcpSocket *Controller::getControlSocket() +{ + return m_controlSocket; } void Controller::postControlEvent(ControlEvent *controlEvent) diff --git a/QtScrcpy/inputcontrol/controller.h b/QtScrcpy/inputcontrol/controller.h index 2fb4c2f..1522f3c 100644 --- a/QtScrcpy/inputcontrol/controller.h +++ b/QtScrcpy/inputcontrol/controller.h @@ -6,6 +6,7 @@ class QTcpSocket; class ControlEvent; +class Receiver; class Controller : public QObject { Q_OBJECT @@ -14,6 +15,7 @@ public: virtual ~Controller(); void setControlSocket(QTcpSocket* controlSocket); + QTcpSocket* getControlSocket(); void postControlEvent(ControlEvent* controlEvent); void test(QRect rc); @@ -25,6 +27,7 @@ private: private: QPointer m_controlSocket; + QPointer m_receiver; }; #endif // CONTROLLER_H diff --git a/QtScrcpy/inputcontrol/deviceevent.cpp b/QtScrcpy/inputcontrol/deviceevent.cpp new file mode 100644 index 0000000..668fbcf --- /dev/null +++ b/QtScrcpy/inputcontrol/deviceevent.cpp @@ -0,0 +1,69 @@ +#include + +#include "deviceevent.h" +#include "bufferutil.h" + +DeviceEvent::DeviceEvent(QObject *parent) : QObject(parent) +{ + +} + +DeviceEvent::~DeviceEvent() +{ + if (DET_GET_CLIPBOARD == m_data.type + && Q_NULLPTR != m_data.clipboardEvent.text) { + delete m_data.clipboardEvent.text; + m_data.clipboardEvent.text = Q_NULLPTR; + } +} + +DeviceEvent::DeviceEventType DeviceEvent::type() +{ + return m_data.type; +} + +void DeviceEvent::getClipboardEventData(QString& text) +{ + text = QString::fromUtf8(m_data.clipboardEvent.text); +} + +qint32 DeviceEvent::deserialize(QByteArray& byteArray) +{ + QBuffer buf(&byteArray); + buf.open(QBuffer::ReadOnly); + + qint64 len = buf.size(); + char c = 0; + qint32 ret = 0; + + if (len < 3) { + // at least type + empty string length + return 0; // not available + } + + buf.getChar(&c); + m_data.type = (DeviceEventType)c; + switch (m_data.type) { + case DET_GET_CLIPBOARD: { + quint16 clipboardLen = BufferUtil::read16(buf); + if (clipboardLen > len - 3) { + ret = 0; // not available + break; + } + + QByteArray text = buf.readAll(); + m_data.clipboardEvent.text = new char[text.length() + 1]; + memcpy(m_data.clipboardEvent.text, text.data(), text.length()); + m_data.clipboardEvent.text[text.length()] = '\0'; + + ret = 3 + clipboardLen; + break; + } + default: + qWarning("Unsupported device event type: %d", (int) m_data.type); + ret = -1; // error, we cannot recover + } + + buf.close(); + return ret; +} diff --git a/QtScrcpy/inputcontrol/deviceevent.h b/QtScrcpy/inputcontrol/deviceevent.h new file mode 100644 index 0000000..7f0be20 --- /dev/null +++ b/QtScrcpy/inputcontrol/deviceevent.h @@ -0,0 +1,42 @@ +#ifndef DEVICEEVENT_H +#define DEVICEEVENT_H + +#include + +#define DEVICE_EVENT_QUEUE_SIZE 64 +#define DEVICE_EVENT_TEXT_MAX_LENGTH 4093 +#define DEVICE_EVENT_SERIALIZED_MAX_SIZE (3 + DEVICE_EVENT_TEXT_MAX_LENGTH) + +class DeviceEvent : public QObject +{ + Q_OBJECT +public: + enum DeviceEventType { + DET_NULL = -1, + // 和服务端对应 + DET_GET_CLIPBOARD = 0, + }; + explicit DeviceEvent(QObject *parent = nullptr); + virtual ~DeviceEvent(); + + DeviceEvent::DeviceEventType type(); + void getClipboardEventData(QString& text); + + qint32 deserialize(QByteArray& byteArray); + +private: + struct DeviceEventData { + DeviceEventType type = DET_NULL; + union { + struct { + char* text = Q_NULLPTR; + } clipboardEvent; + }; + DeviceEventData(){} + ~DeviceEventData(){} + }; + + DeviceEventData m_data; +}; + +#endif // DEVICEEVENT_H diff --git a/QtScrcpy/inputcontrol/inputcontrol.pri b/QtScrcpy/inputcontrol/inputcontrol.pri index 1445e3e..af7fc9c 100644 --- a/QtScrcpy/inputcontrol/inputcontrol.pri +++ b/QtScrcpy/inputcontrol/inputcontrol.pri @@ -3,12 +3,16 @@ HEADERS += \ $$PWD/controller.h \ $$PWD/inputconvertbase.h \ $$PWD/inputconvertgame.h \ - $$PWD/inputconvertnormal.h + $$PWD/inputconvertnormal.h \ + $$PWD/deviceevent.h \ + $$PWD/receiver.h SOURCES += \ $$PWD/controlevent.cpp \ $$PWD/controller.cpp \ $$PWD/inputconvertbase.cpp \ $$PWD/inputconvertgame.cpp \ - $$PWD/inputconvertnormal.cpp + $$PWD/inputconvertnormal.cpp \ + $$PWD/deviceevent.cpp \ + $$PWD/receiver.cpp diff --git a/QtScrcpy/inputcontrol/receiver.cpp b/QtScrcpy/inputcontrol/receiver.cpp new file mode 100644 index 0000000..51c8cc0 --- /dev/null +++ b/QtScrcpy/inputcontrol/receiver.cpp @@ -0,0 +1,53 @@ +#include +#include +#include + +#include "receiver.h" +#include "controller.h" +#include "deviceevent.h" + +Receiver::Receiver(Controller* controller) : QObject(controller) +{ + m_controller = controller; + Q_ASSERT(controller); +} + +Receiver::~Receiver() +{ + +} + +void Receiver::onReadyRead() +{ + QTcpSocket* controlSocket = m_controller->getControlSocket(); + if (!controlSocket) { + return; + } + + while (controlSocket->bytesAvailable()) { + QByteArray byteArray = controlSocket->peek(controlSocket->bytesAvailable()); + DeviceEvent deviceEvent; + qint32 consume = deviceEvent.deserialize(byteArray); + if (0 >= consume) { + break; + } + controlSocket->read(consume); + processEvent(&deviceEvent); + } +} + +void Receiver::processEvent(DeviceEvent *deviceEvent) +{ + switch (deviceEvent->type()) { + case DeviceEvent::DET_GET_CLIPBOARD: + { + QClipboard *board = QApplication::clipboard(); + QString text; + deviceEvent->getClipboardEventData(text); + board->setText(text); + break; + } + default: + break; + } +} diff --git a/QtScrcpy/inputcontrol/receiver.h b/QtScrcpy/inputcontrol/receiver.h new file mode 100644 index 0000000..ba9bc4c --- /dev/null +++ b/QtScrcpy/inputcontrol/receiver.h @@ -0,0 +1,25 @@ +#ifndef RECEIVER_H +#define RECEIVER_H + +#include + +class Controller; +class DeviceEvent; +class Receiver : public QObject +{ + Q_OBJECT +public: + explicit Receiver(Controller *controller); + virtual ~Receiver(); + +public slots: + void onReadyRead(); + +protected: + void processEvent(DeviceEvent *deviceEvent); + +private: + QPointer m_controller; +}; + +#endif // RECEIVER_H diff --git a/QtScrcpy/util/bufferutil.cpp b/QtScrcpy/util/bufferutil.cpp new file mode 100644 index 0000000..65f62d2 --- /dev/null +++ b/QtScrcpy/util/bufferutil.cpp @@ -0,0 +1,51 @@ +#include "bufferutil.h" + +void BufferUtil::write32(QBuffer &buffer, quint32 value) +{ + buffer.putChar(value >> 24); + buffer.putChar(value >> 16); + buffer.putChar(value >> 8); + buffer.putChar(value); +} + +void BufferUtil::write16(QBuffer &buffer, quint32 value) +{ + buffer.putChar(value >> 8); + buffer.putChar(value); +} + +quint16 BufferUtil::read16(QBuffer &buffer) +{ + char c; + quint16 ret = 0; + buffer.getChar(&c); + ret |= (c << 8); + buffer.getChar(&c); + ret |= c; + + return ret; +} + +quint32 BufferUtil::read32(QBuffer &buffer) +{ + char c; + quint32 ret = 0; + buffer.getChar(&c); + ret |= (c << 24); + buffer.getChar(&c); + ret |= (c << 16); + buffer.getChar(&c); + ret |= (c << 8); + buffer.getChar(&c); + ret |= c; + + return ret; +} + +quint64 BufferUtil::read64(QBuffer &buffer) +{ + quint32 msb = read32(buffer); + quint32 lsb = read32(buffer); + + return ((quint64) msb << 32) | lsb;; +} diff --git a/QtScrcpy/util/bufferutil.h b/QtScrcpy/util/bufferutil.h new file mode 100644 index 0000000..39152ac --- /dev/null +++ b/QtScrcpy/util/bufferutil.h @@ -0,0 +1,15 @@ +#ifndef BUFFERUTIL_H +#define BUFFERUTIL_H +#include + +class BufferUtil +{ +public: + static void write32(QBuffer& buffer, quint32 value); + static void write16(QBuffer& buffer, quint32 value); + static quint16 read16(QBuffer& buffer); + static quint32 read32(QBuffer& buffer); + static quint64 read64(QBuffer& buffer); +}; + +#endif // BUFFERUTIL_H diff --git a/QtScrcpy/util/util.pri b/QtScrcpy/util/util.pri index 366d5c5..720d7e6 100644 --- a/QtScrcpy/util/util.pri +++ b/QtScrcpy/util/util.pri @@ -1,4 +1,8 @@ include ($$PWD/mousetap/mousetap.pri) HEADERS += \ - $$PWD/compat.h + $$PWD/compat.h \ + $$PWD/bufferutil.h + +SOURCES += \ + $$PWD/bufferutil.cpp diff --git a/QtScrcpy/videoform.cpp b/QtScrcpy/videoform.cpp index d8c2364..fcc455f 100644 --- a/QtScrcpy/videoform.cpp +++ b/QtScrcpy/videoform.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "videoform.h" #include "recorder.h" @@ -365,7 +366,35 @@ void VideoForm::collapseNotificationPanel() m_inputConvert.sendControlEvent(controlEvent); } -void VideoForm::postTextInput(const QString& text) +void VideoForm::requestDeviceClipboard() +{ + ControlEvent* controlEvent = new ControlEvent(ControlEvent::CET_GET_CLIPBOARD); + if (!controlEvent) { + return; + } + m_inputConvert.sendControlEvent(controlEvent); +} + +void VideoForm::setDeviceClipboard() +{ + QClipboard *board = QApplication::clipboard(); + QString text = board->text(); + ControlEvent* controlEvent = new ControlEvent(ControlEvent::CET_SET_CLIPBOARD); + if (!controlEvent) { + return; + } + controlEvent->setSetClipboardEventData(text); + m_inputConvert.sendControlEvent(controlEvent); +} + +void VideoForm::clipboardPaste() +{ + QClipboard *board = QApplication::clipboard(); + QString text = board->text(); + postTextInput(text); +} + +void VideoForm::postTextInput(QString& text) { ControlEvent* controlEvent = new ControlEvent(ControlEvent::CET_TEXT); if (!controlEvent) { @@ -468,6 +497,16 @@ void VideoForm::keyPressEvent(QKeyEvent *event) && isFullScreen()) { switchFullScreen(); } + if (event->key() == Qt::Key_C && (event->modifiers() & Qt::ControlModifier)) { + requestDeviceClipboard(); + } + if (event->key() == Qt::Key_V && (event->modifiers() & Qt::ControlModifier)) { + if (event->modifiers() & Qt::ShiftModifier) { + setDeviceClipboard(); + } else { + clipboardPaste(); + } + } //qDebug() << "keyPressEvent" << event->isAutoRepeat(); m_inputConvert.keyEvent(event, ui->videoWidget->frameSize(), ui->videoWidget->size()); diff --git a/QtScrcpy/videoform.h b/QtScrcpy/videoform.h index c8c1c89..1b229c7 100644 --- a/QtScrcpy/videoform.h +++ b/QtScrcpy/videoform.h @@ -39,7 +39,10 @@ public: void postTurnOn(); void expandNotificationPanel(); void collapseNotificationPanel(); - void postTextInput(const QString& text); + void requestDeviceClipboard(); + void setDeviceClipboard(); + void clipboardPaste(); + void postTextInput(QString& text); void staysOnTop(bool top = true); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java index b368d31..1196ea1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEvent.java @@ -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; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java index 6b9e8ea..f4688c0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlEventReader.java @@ -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()); diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index e7237fa..99aa9d2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -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); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index c74844e..93b0302 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -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); } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java b/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java new file mode 100644 index 0000000..97bcbfc --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceEvent.java @@ -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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java new file mode 100644 index 0000000..e183a22 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceEventWriter.java @@ -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; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/EventController.java b/server/src/main/java/com/genymobile/scrcpy/EventController.java index 34f555a..0bf6055 100644 --- a/server/src/main/java/com/genymobile/scrcpy/EventController.java +++ b/server/src/main/java/com/genymobile/scrcpy/EventController.java @@ -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; diff --git a/server/src/main/java/com/genymobile/scrcpy/EventSender.java b/server/src/main/java/com/genymobile/scrcpy/EventSender.java new file mode 100644 index 0000000..9f50b16 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/EventSender.java @@ -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); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 479e6b6..ed8a40f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -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) { diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java new file mode 100644 index 0000000..199fc8c --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -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 + 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; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java new file mode 100644 index 0000000..a058a8b --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -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); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 3bcdc0e..0b625c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -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; + } } diff --git a/third_party/scrcpy-server.jar b/third_party/scrcpy-server.jar index 4637040..5f8ad87 100644 Binary files a/third_party/scrcpy-server.jar and b/third_party/scrcpy-server.jar differ