add:实现剪切板功能

This commit is contained in:
Barry 2019-06-19 14:33:20 +08:00
commit b33b22bf16
27 changed files with 650 additions and 56 deletions

View file

@ -1,6 +1,7 @@
#include <QDebug> #include <QDebug>
#include "controlevent.h" #include "controlevent.h"
#include "bufferutil.h"
ControlEvent::ControlEvent(ControlEventType controlEventType) ControlEvent::ControlEvent(ControlEventType controlEventType)
: QScrcpyEvent(Control) : QScrcpyEvent(Control)
@ -8,6 +9,19 @@ ControlEvent::ControlEvent(ControlEventType controlEventType)
m_data.type = 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) void ControlEvent::setKeycodeEventData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate)
{ {
m_data.keycodeEvent.action = action; m_data.keycodeEvent.action = action;
@ -15,16 +29,17 @@ void ControlEvent::setKeycodeEventData(AndroidKeyeventAction action, AndroidKeyc
m_data.keycodeEvent.metastate = metastate; m_data.keycodeEvent.metastate = metastate;
} }
void ControlEvent::setTextEventData(QString text) void ControlEvent::setTextEventData(QString& text)
{ {
// write length (2 byte) + string (non nul-terminated) // 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 // 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(); 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()); 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) 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; m_data.scrollEvent.vScroll = vScroll;
} }
void ControlEvent::write32(QBuffer &buffer, quint32 value) void ControlEvent::setSetClipboardEventData(QString &text)
{ {
buffer.putChar(value >> 24); if (text.isEmpty()) {
buffer.putChar(value >> 16); return;
buffer.putChar(value >> 8); }
buffer.putChar(value); 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) QByteArray tmp = text.toUtf8();
{ m_data.setClipboardEvent.text = new char[tmp.length() + 1];
buffer.putChar(value >> 8); memcpy(m_data.setClipboardEvent.text, tmp.data(), tmp.length());
buffer.putChar(value); m_data.setClipboardEvent.text[tmp.length()] = '\0';
} }
void ControlEvent::writePosition(QBuffer &buffer, const QRect& value) void ControlEvent::writePosition(QBuffer &buffer, const QRect& value)
{ {
write16(buffer, value.left()); BufferUtil::write16(buffer, value.left());
write16(buffer, value.top()); BufferUtil::write16(buffer, value.top());
write16(buffer, value.width()); BufferUtil::write16(buffer, value.width());
write16(buffer, value.height()); BufferUtil::write16(buffer, value.height());
} }
QByteArray ControlEvent::serializeData() QByteArray ControlEvent::serializeData()
@ -80,18 +96,16 @@ QByteArray ControlEvent::serializeData()
switch (m_data.type) { switch (m_data.type) {
case CET_KEYCODE: case CET_KEYCODE:
buffer.putChar(m_data.keycodeEvent.action); buffer.putChar(m_data.keycodeEvent.action);
write32(buffer, m_data.keycodeEvent.keycode); BufferUtil::write32(buffer, m_data.keycodeEvent.keycode);
write32(buffer, m_data.keycodeEvent.metastate); BufferUtil::write32(buffer, m_data.keycodeEvent.metastate);
break; break;
case CET_TEXT: case CET_TEXT:
{ BufferUtil::write16(buffer, strlen(m_data.textEvent.text));
write16(buffer, strlen(m_data.textEvent.text));
buffer.write(m_data.textEvent.text, strlen(m_data.textEvent.text)); buffer.write(m_data.textEvent.text, strlen(m_data.textEvent.text));
}
break; break;
case CET_MOUSE: case CET_MOUSE:
buffer.putChar(m_data.mouseEvent.action); 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); writePosition(buffer, m_data.mouseEvent.position);
break; break;
case CET_TOUCH: case CET_TOUCH:
@ -101,12 +115,17 @@ QByteArray ControlEvent::serializeData()
break; break;
case CET_SCROLL: case CET_SCROLL:
writePosition(buffer, m_data.scrollEvent.position); writePosition(buffer, m_data.scrollEvent.position);
write32(buffer, m_data.scrollEvent.hScroll); BufferUtil::write32(buffer, m_data.scrollEvent.hScroll);
write32(buffer, m_data.scrollEvent.vScroll); 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; break;
case CET_BACK_OR_SCREEN_ON: case CET_BACK_OR_SCREEN_ON:
case CET_EXPAND_NOTIFICATION_PANEL: case CET_EXPAND_NOTIFICATION_PANEL:
case CET_COLLAPSE_NOTIFICATION_PANEL: case CET_COLLAPSE_NOTIFICATION_PANEL:
case CET_GET_CLIPBOARD:
break; break;
default: default:
qDebug() << "Unknown event type:" << m_data.type; qDebug() << "Unknown event type:" << m_data.type;

View file

@ -9,43 +9,48 @@
#include "input.h" #include "input.h"
#include "keycodes.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 // ControlEvent
class ControlEvent : public QScrcpyEvent class ControlEvent : public QScrcpyEvent
{ {
public: public:
enum ControlEventType { enum ControlEventType {
CET_NULL = -1,
CET_KEYCODE = 0, CET_KEYCODE = 0,
CET_TEXT, CET_TEXT,
CET_MOUSE, CET_MOUSE,
CET_SCROLL, CET_SCROLL,
CET_TOUCH,
CET_BACK_OR_SCREEN_ON, CET_BACK_OR_SCREEN_ON,
CET_EXPAND_NOTIFICATION_PANEL, CET_EXPAND_NOTIFICATION_PANEL,
CET_COLLAPSE_NOTIFICATION_PANEL, CET_COLLAPSE_NOTIFICATION_PANEL,
CET_GET_CLIPBOARD,
CET_SET_CLIPBOARD,
CET_TOUCH,
}; };
ControlEvent(ControlEventType controlEventType); ControlEvent(ControlEventType controlEventType);
virtual ~ControlEvent();
void setKeycodeEventData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate); void setKeycodeEventData(AndroidKeyeventAction action, AndroidKeycode keycode, AndroidMetastate metastate);
void setTextEventData(QString text); void setTextEventData(QString& text);
void setMouseEventData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position); void setMouseEventData(AndroidMotioneventAction action, AndroidMotioneventButtons buttons, QRect position);
// id 代表一个触摸点最多支持10个触摸点[0,9] // id 代表一个触摸点最多支持10个触摸点[0,9]
// action 只能是AMOTION_EVENT_ACTION_DOWNAMOTION_EVENT_ACTION_UPAMOTION_EVENT_ACTION_MOVE // action 只能是AMOTION_EVENT_ACTION_DOWNAMOTION_EVENT_ACTION_UPAMOTION_EVENT_ACTION_MOVE
// position action动作对应的位置 // position action动作对应的位置
void setTouchEventData(quint32 id, AndroidMotioneventAction action, QRect position); void setTouchEventData(quint32 id, AndroidMotioneventAction action, QRect position);
void setScrollEventData(QRect position, qint32 hScroll, qint32 vScroll); void setScrollEventData(QRect position, qint32 hScroll, qint32 vScroll);
void setSetClipboardEventData(QString& text);
QByteArray serializeData(); QByteArray serializeData();
private: private:
void write32(QBuffer& buffer, quint32 value);
void write16(QBuffer& buffer, quint32 value);
void writePosition(QBuffer& buffer, const QRect& value); void writePosition(QBuffer& buffer, const QRect& value);
private: private:
struct ControlEventData { struct ControlEventData {
ControlEventType type; ControlEventType type = CET_NULL;
union { union {
struct { struct {
AndroidKeyeventAction action; AndroidKeyeventAction action;
@ -53,7 +58,7 @@ private:
AndroidMetastate metastate; AndroidMetastate metastate;
} keycodeEvent; } keycodeEvent;
struct { struct {
char text[TEXT_MAX_CHARACTER_LENGTH + 1]; char* text = Q_NULLPTR;
} textEvent; } textEvent;
struct { struct {
AndroidMotioneventAction action; AndroidMotioneventAction action;
@ -70,6 +75,9 @@ private:
qint32 hScroll; qint32 hScroll;
qint32 vScroll; qint32 vScroll;
} scrollEvent; } scrollEvent;
struct {
char *text = Q_NULLPTR;
} setClipboardEvent;
}; };
ControlEventData(){} ControlEventData(){}

View file

@ -3,10 +3,11 @@
#include "controller.h" #include "controller.h"
#include "videosocket.h" #include "videosocket.h"
#include "controlevent.h" #include "controlevent.h"
#include "receiver.h"
Controller::Controller(QObject* parent) : QObject(parent) Controller::Controller(QObject* parent) : QObject(parent)
{ {
m_receiver = new Receiver(this);
} }
Controller::~Controller() Controller::~Controller()
@ -16,7 +17,16 @@ Controller::~Controller()
void Controller::setControlSocket(QTcpSocket* controlSocket) void Controller::setControlSocket(QTcpSocket* controlSocket)
{ {
if (m_controlSocket || !controlSocket) {
return;
}
m_controlSocket = controlSocket; m_controlSocket = controlSocket;
connect(controlSocket, &QTcpSocket::readyRead, m_receiver, &Receiver::onReadyRead);
}
QTcpSocket *Controller::getControlSocket()
{
return m_controlSocket;
} }
void Controller::postControlEvent(ControlEvent *controlEvent) void Controller::postControlEvent(ControlEvent *controlEvent)

View file

@ -6,6 +6,7 @@
class QTcpSocket; class QTcpSocket;
class ControlEvent; class ControlEvent;
class Receiver;
class Controller : public QObject class Controller : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -14,6 +15,7 @@ public:
virtual ~Controller(); virtual ~Controller();
void setControlSocket(QTcpSocket* controlSocket); void setControlSocket(QTcpSocket* controlSocket);
QTcpSocket* getControlSocket();
void postControlEvent(ControlEvent* controlEvent); void postControlEvent(ControlEvent* controlEvent);
void test(QRect rc); void test(QRect rc);
@ -25,6 +27,7 @@ private:
private: private:
QPointer<QTcpSocket> m_controlSocket; QPointer<QTcpSocket> m_controlSocket;
QPointer<Receiver> m_receiver;
}; };
#endif // CONTROLLER_H #endif // CONTROLLER_H

View file

@ -0,0 +1,69 @@
#include <QDebug>
#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;
}

View file

@ -0,0 +1,42 @@
#ifndef DEVICEEVENT_H
#define DEVICEEVENT_H
#include <QBuffer>
#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

View file

@ -3,12 +3,16 @@ HEADERS += \
$$PWD/controller.h \ $$PWD/controller.h \
$$PWD/inputconvertbase.h \ $$PWD/inputconvertbase.h \
$$PWD/inputconvertgame.h \ $$PWD/inputconvertgame.h \
$$PWD/inputconvertnormal.h $$PWD/inputconvertnormal.h \
$$PWD/deviceevent.h \
$$PWD/receiver.h
SOURCES += \ SOURCES += \
$$PWD/controlevent.cpp \ $$PWD/controlevent.cpp \
$$PWD/controller.cpp \ $$PWD/controller.cpp \
$$PWD/inputconvertbase.cpp \ $$PWD/inputconvertbase.cpp \
$$PWD/inputconvertgame.cpp \ $$PWD/inputconvertgame.cpp \
$$PWD/inputconvertnormal.cpp $$PWD/inputconvertnormal.cpp \
$$PWD/deviceevent.cpp \
$$PWD/receiver.cpp

View file

@ -0,0 +1,53 @@
#include <QTcpSocket>
#include <QApplication>
#include <QClipboard>
#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;
}
}

View file

@ -0,0 +1,25 @@
#ifndef RECEIVER_H
#define RECEIVER_H
#include <QPointer>
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<Controller> m_controller;
};
#endif // RECEIVER_H

View file

@ -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;;
}

View file

@ -0,0 +1,15 @@
#ifndef BUFFERUTIL_H
#define BUFFERUTIL_H
#include <QBuffer>
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

View file

@ -1,4 +1,8 @@
include ($$PWD/mousetap/mousetap.pri) include ($$PWD/mousetap/mousetap.pri)
HEADERS += \ HEADERS += \
$$PWD/compat.h $$PWD/compat.h \
$$PWD/bufferutil.h
SOURCES += \
$$PWD/bufferutil.cpp

View file

@ -11,6 +11,7 @@
#include <QMimeData> #include <QMimeData>
#include <QFileInfo> #include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QClipboard>
#include "videoform.h" #include "videoform.h"
#include "recorder.h" #include "recorder.h"
@ -365,7 +366,35 @@ void VideoForm::collapseNotificationPanel()
m_inputConvert.sendControlEvent(controlEvent); 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); ControlEvent* controlEvent = new ControlEvent(ControlEvent::CET_TEXT);
if (!controlEvent) { if (!controlEvent) {
@ -468,6 +497,16 @@ void VideoForm::keyPressEvent(QKeyEvent *event)
&& isFullScreen()) { && isFullScreen()) {
switchFullScreen(); 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(); //qDebug() << "keyPressEvent" << event->isAutoRepeat();
m_inputConvert.keyEvent(event, ui->videoWidget->frameSize(), ui->videoWidget->size()); m_inputConvert.keyEvent(event, ui->videoWidget->frameSize(), ui->videoWidget->size());

View file

@ -39,7 +39,10 @@ public:
void postTurnOn(); void postTurnOn();
void expandNotificationPanel(); void expandNotificationPanel();
void collapseNotificationPanel(); void collapseNotificationPanel();
void postTextInput(const QString& text); void requestDeviceClipboard();
void setDeviceClipboard();
void clipboardPaste();
void postTextInput(QString& text);
void staysOnTop(bool top = true); void staysOnTop(bool top = true);

View file

@ -9,10 +9,13 @@ public final class ControlEvent {
public static final int TYPE_TEXT = 1; public static final int TYPE_TEXT = 1;
public static final int TYPE_MOUSE = 2; public static final int TYPE_MOUSE = 2;
public static final int TYPE_SCROLL = 3; public static final int TYPE_SCROLL = 3;
public static final int TYPE_TOUCH = 4; public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_BACK_OR_SCREEN_ON = 5; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 6; public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 7; 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; private int type;
@ -72,6 +75,13 @@ public final class ControlEvent {
return event; 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) { public static ControlEvent createSimpleControlEvent(int type) {
ControlEvent event = new ControlEvent(); ControlEvent event = new ControlEvent();
event.type = type; event.type = type;

View file

@ -14,11 +14,12 @@ public class ControlEventReader {
private static final int SCROLL_PAYLOAD_LENGTH = 16; private static final int SCROLL_PAYLOAD_LENGTH = 16;
public static final int TEXT_MAX_LENGTH = 300; 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 static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); 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() { public ControlEventReader() {
// invariant: the buffer is always in "get" mode // invariant: the buffer is always in "get" mode
@ -66,9 +67,13 @@ public class ControlEventReader {
case ControlEvent.TYPE_SCROLL: case ControlEvent.TYPE_SCROLL:
controlEvent = parseScrollControlEvent(); controlEvent = parseScrollControlEvent();
break; break;
case ControlEvent.TYPE_SET_CLIPBOARD:
controlEvent = parseSetClipboardEvent();
break;
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:
@ -94,8 +99,8 @@ public class ControlEventReader {
return ControlEvent.createKeycodeControlEvent(action, keycode, metaState); return ControlEvent.createKeycodeControlEvent(action, keycode, metaState);
} }
private ControlEvent parseTextControlEvent() { private String parseString() {
if (buffer.remaining() < 1) { if (buffer.remaining() < 2) {
return null; return null;
} }
int len = toUnsigned(buffer.getShort()); int len = toUnsigned(buffer.getShort());
@ -103,7 +108,14 @@ public class ControlEventReader {
return null; return null;
} }
buffer.get(textBuffer, 0, len); 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); return ControlEvent.createTextControlEvent(text);
} }
@ -137,6 +149,14 @@ public class ControlEventReader {
return ControlEvent.createScrollControlEvent(position, hScroll, vScroll); 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) { private static Position readPosition(ByteBuffer buffer) {
int x = toUnsigned(buffer.getShort()); int x = toUnsigned(buffer.getShort());
int y = toUnsigned(buffer.getShort()); int y = toUnsigned(buffer.getShort());

View file

@ -8,6 +8,7 @@ import java.io.Closeable;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public final class DesktopConnection implements Closeable { public final class DesktopConnection implements Closeable {
@ -21,14 +22,17 @@ public final class DesktopConnection implements Closeable {
private final LocalSocket controlSocket; private final LocalSocket controlSocket;
private final InputStream controlInputStream; private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
private final ControlEventReader reader = new ControlEventReader(); private final ControlEventReader reader = new ControlEventReader();
private final DeviceEventWriter writer = new DeviceEventWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket; this.videoSocket = videoSocket;
this.controlSocket = controlSocket; this.controlSocket = controlSocket;
controlInputStream = controlSocket.getInputStream(); controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
videoFd = videoSocket.getFileDescriptor(); videoFd = videoSocket.getFileDescriptor();
} }
@ -109,4 +113,8 @@ public final class DesktopConnection implements Closeable {
} }
return event; return event;
} }
public void sendDeviceEvent(DeviceEvent event) throws IOException {
writer.writeTo(event, controlOutputStream);
}
} }

View file

@ -180,6 +180,18 @@ 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();
}
public void setClipboardText(String text) {
serviceManager.getClipboardManager().setText(text);
}
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);
} }

View 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;
}
}

View file

@ -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;
}
}
}

View file

@ -13,11 +13,11 @@ import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
import java.util.Vector; import java.util.Vector;
public class EventController { public class EventController {
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final EventSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@ -28,6 +28,7 @@ public class EventController {
public EventController(Device device, DesktopConnection connection) { public EventController(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
sender = new EventSender(connection);
} }
private int getPointer(int id) { private int getPointer(int id) {
@ -97,6 +98,10 @@ public class EventController {
} }
} }
public EventSender getSender() {
return sender;
}
public void control() throws IOException { public void control() throws IOException {
// on start, turn screen on // on start, turn screen on
turnScreenOn(); turnScreenOn();
@ -133,6 +138,13 @@ 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;
case ControlEvent.TYPE_SET_CLIPBOARD:
device.setClipboardText(controlEvent.getText());
break;
default: default:
// do nothing // do nothing
} }
@ -144,7 +156,7 @@ public class EventController {
private boolean injectChar(char c) { private boolean injectChar(char c) {
String decomposed = KeyComposition.decompose(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); KeyEvent[] events = charMap.getEvents(chars);
if (events == null) { if (events == null) {
return false; return false;

View 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);
}
}
}

View file

@ -20,8 +20,11 @@ public final class Server {
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
EventController controller = new EventController(device, connection);
// asynchronous // asynchronous
startEventController(device, connection); startEventController(controller);
startEventSender(controller.getSender());
try { try {
// synchronous // 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() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
new EventController(device, connection).control(); controller.control();
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
Ln.d("Event controller stopped"); Ln.d("Event controller stopped");
@ -47,6 +50,20 @@ public final class Server {
}).start(); }).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") @SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) { private static Options createOptions(String... args) {
if (args.length != 5) { if (args.length != 5) {

View 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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
} }

Binary file not shown.